change!: vehicle_data now accepts a struct instead of VehicleId

To support "endpoints", e.g. requesting GPS.

From https://developer.tesla.com/docs/fleet-api#vehicle_data

String of URL-encoded, semicolon-separated values. Can be many of 'charge_state',
'climate_state', 'closures_state', 'drive_state', 'gui_settings', 'location_data',
'vehicle_config', 'vehicle_state', 'vehicle_data_combo'.

Before:

let vehicle_data = api.vehicle_data(&vehicle_id).await.unwrap();

After:

let get_vehicle_data = GetVehicleData::new(vehicles_id);
let vehicle_data = api.vehicle_data(&get_vehicle_data).await.unwrap();

Or with a endpoints:

let get_vehicle_data = GetVehicleData::new_with_endpoints(123u64, vec![Endpoint::ChargeState, Endpoint::ClimateState]);
let vehicle_data = api.vehicle_data(&get_vehicle_data).await.unwrap();

CLI:

You can still use vehicle-data without endpoints, but you won't get
location data. To fetch location_data:

teslatte api vehicle 123 vehicle-data location_data
This commit is contained in:
gak 2023-11-11 11:07:24 +11:00
parent f0f4a61fa2
commit 6facc27d8b
No known key found for this signature in database
7 changed files with 139 additions and 29 deletions

View file

@ -1,6 +1,7 @@
use std::env;
use teslatte::auth::AccessToken;
use teslatte::products::Product;
use teslatte::vehicles::GetVehicleData;
use teslatte::{OwnerApi, VehicleApi};
#[tokio::main]
@ -20,7 +21,8 @@ async fn main() {
dbg!(&*vehicles);
if !vehicles.is_empty() {
let vehicle_data = api.vehicle_data(&vehicles[0].id).await.unwrap();
let get_vehicle_data = GetVehicleData::new(vehicles[0].id.clone());
let vehicle_data = api.vehicle_data(&get_vehicle_data).await.unwrap();
dbg!(&vehicle_data);
} else {
println!("No vehicles found!");

View file

@ -1,5 +1,6 @@
use crate::vehicles::{
SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures,
Endpoints, GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging,
SetScheduledDeparture, SetTemperatures,
};
use crate::{OwnerApi, VehicleApi, VehicleId};
use clap::{Args, Subcommand};
@ -7,7 +8,7 @@ use clap::{Args, Subcommand};
#[derive(Debug, Subcommand)]
pub enum VehicleCommand {
/// Get vehicle data.
VehicleData,
VehicleData(Endpoints),
/// Open the charge port door or unlocks the cable.
ChargePortDoorOpen,
@ -75,8 +76,9 @@ pub struct VehicleArgs {
impl VehicleArgs {
pub async fn run(self, api: &OwnerApi) -> miette::Result<()> {
match self.command {
VehicleCommand::VehicleData => {
api.vehicle_data(&self.id).await?;
VehicleCommand::VehicleData(endpoints) => {
let get_vehicle_data = GetVehicleData::new_with_endpoints(self.id, endpoints);
api.vehicle_data(&get_vehicle_data).await?;
}
VehicleCommand::SetChargeLimit(limit) => {
api.set_charge_limit(&self.id, &limit).await?;

View file

@ -1,5 +1,5 @@
use crate::products::EnergySiteId;
use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, OwnerApi, Values};
use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, ApiValues, OwnerApi};
use chrono::{DateTime, FixedOffset};
use serde::Deserialize;
use strum::{Display, EnumString, IntoStaticStr};
@ -194,7 +194,7 @@ pub struct CalendarHistoryValues {
pub end_date: Option<DateTime<FixedOffset>>,
}
impl Values for CalendarHistoryValues {
impl ApiValues for CalendarHistoryValues {
fn format(&self, url: &str) -> String {
let url = url.replace("{}", &format!("{}", self.site_id.0));
let mut pairs: Vec<(&str, String)> = vec![

View file

@ -5,11 +5,11 @@
use crate::auth::{AccessToken, RefreshToken};
use crate::error::TeslatteError;
use crate::vehicles::{
SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures,
Vehicle, VehicleData,
GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture,
SetTemperatures, Vehicle, VehicleData,
};
use chrono::{DateTime, SecondsFormat, TimeZone};
use derive_more::{Display, FromStr};
use derive_more::{Deref, Display, From, FromStr};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
@ -29,7 +29,10 @@ const API_URL: &str = "https://owner-api.teslamotors.com/api/1";
pub trait VehicleApi {
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
async fn vehicle_data(&self, vehicle_id: &VehicleId) -> Result<VehicleData, TeslatteError>;
async fn vehicle_data(
&self,
get_vehicle_data: &GetVehicleData,
) -> Result<VehicleData, TeslatteError>;
async fn wake_up(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
// Alerts
@ -97,16 +100,22 @@ pub trait VehicleApi {
trait EnergySitesApi {}
trait Values {
trait ApiValues {
fn format(&self, url: &str) -> String;
}
/// Vehicle ID used by the owner-api endpoint.
///
/// This data comes from [`OwnerApi::vehicles()`] `id` field.
#[derive(Debug, Serialize, Deserialize, Clone, Display, FromStr)]
#[derive(Debug, Serialize, Deserialize, Clone, Display, FromStr, From, Deref)]
pub struct VehicleId(u64);
impl VehicleId {
pub fn new(id: u64) -> Self {
Self(id)
}
}
/// Vehicle ID used by other endpoints.
///
/// This data comes from [`OwnerApi::vehicles()`] `vehicle_id` field.
@ -333,6 +342,7 @@ pub(crate) use pub_get;
/// GET /api/1/[url] with an argument.
///
/// Pass in the URL as a format string with one arg, which has to impl Display.
#[allow(unused_macros)]
macro_rules! get_arg {
($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => {
async fn $name(
@ -345,6 +355,7 @@ macro_rules! get_arg {
}
};
}
#[allow(unused_imports)]
pub(crate) use get_arg;
/// Public variant of get_arg.
@ -362,8 +373,7 @@ macro_rules! pub_get_arg {
}
pub(crate) use pub_get_arg;
/// GET /api/1/[url] with a struct.
#[allow(unused)] // Leaving this here for now. I'm sure it'll be used during this refactor.
/// GET /api/1/[url] with a struct to format the URL.
macro_rules! get_args {
($name:ident, $return_type:ty, $url:expr, $args:ty) => {
async fn $name(
@ -376,7 +386,6 @@ macro_rules! get_args {
}
};
}
#[allow(unused)] // Leaving this here for now. I'm sure it'll be used during this refactor.
pub(crate) use get_args;
/// Public variant of get_args.

View file

@ -1,6 +1,6 @@
use crate::energy_sites::{HistoryKind, HistoryPeriod};
use crate::products::GatewayId;
use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, OwnerApi, Values};
use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, ApiValues, OwnerApi};
use chrono::{DateTime, FixedOffset};
use derive_more::{Display, FromStr};
use serde::{Deserialize, Serialize};
@ -33,7 +33,7 @@ pub struct PowerwallEnergyHistoryValues {
pub end_date: Option<DateTime<FixedOffset>>,
}
impl Values for PowerwallEnergyHistoryValues {
impl ApiValues for PowerwallEnergyHistoryValues {
fn format(&self, url: &str) -> String {
let url = url.replace("{}", &self.powerwall_id.0.to_string());
let mut pairs: Vec<(&str, String)> = vec![

View file

@ -88,7 +88,7 @@ pub struct Components {
mod tests {
use super::*;
use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
use crate::Values;
use crate::ApiValues;
use chrono::DateTime;
#[test]

View file

@ -1,17 +1,19 @@
/// Please note that many of these structs are generated from my own API call responses.
///
/// Sometimes the API will return a null for a field where I've put in a non Option type, which
/// will cause the deserializer to fail. Please log an issue to fix these if you come across it.
use derive_more::{Deref, DerefMut, From};
// Please note that many of these structs are generated from my own API call responses.
//
// Sometimes the API will return a null for a field where I've put in a non Option type, which
// will cause the deserializer to fail. Please log an issue to fix these if you come across it.
use crate::{
get, get_arg, post_arg, post_arg_empty, Empty, ExternalVehicleId, OwnerApi, VehicleApi,
VehicleId,
get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi,
VehicleApi, VehicleId,
};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[rustfmt::skip]
impl VehicleApi for OwnerApi {
get!(vehicles, Vec<Vehicle>, "/vehicles");
get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId);
get_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData);
post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId);
// Alerts
@ -41,6 +43,84 @@ impl VehicleApi for OwnerApi {
post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", VehicleId);
}
#[derive(Debug, Clone, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum Endpoint {
ChargeState,
ClimateState,
ClosuresState,
DriveState,
GuiSettings,
LocationData,
VehicleConfig,
VehicleState,
VehicleDataCombo,
}
#[derive(Debug, Clone, From, Deref, DerefMut)]
#[cfg_attr(feature = "cli", derive(clap::Args))]
pub struct Endpoints {
endpoints: Vec<Endpoint>,
}
pub struct GetVehicleData {
pub vehicle_id: VehicleId,
/// From https://developer.tesla.com/docs/fleet-api#vehicle_data
/// String of URL-encoded, semicolon-separated values. Can be many of 'charge_state',
/// 'climate_state', 'closures_state', 'drive_state', 'gui_settings', 'location_data',
/// 'vehicle_config', 'vehicle_state', 'vehicle_data_combo'.
pub endpoints: Endpoints,
}
impl GetVehicleData {
/// Create a new GetVehicleData request with no endpoints.
///
/// ```rust
/// # use teslatte::VehicleId;
/// # use teslatte::vehicles::GetVehicleData;
/// let get_vehicle_data = GetVehicleData::new(123u64);
/// let get_vehicle_data = GetVehicleData::new(VehicleId::new(123u64));
/// ```
pub fn new(vehicle_id: impl Into<VehicleId>) -> Self {
Self::new_with_endpoints(vehicle_id, vec![])
}
/// Create a new GetVehicleData request with endpoints.
/// ```rust
/// # use teslatte::vehicles::{Endpoint, GetVehicleData};
/// let get_vehicle_data = GetVehicleData::new_with_endpoints(123u64, vec![Endpoint::ChargeState, Endpoint::ClimateState]);
/// ```
pub fn new_with_endpoints(
vehicle_id: impl Into<VehicleId>,
endpoints: impl Into<Endpoints>,
) -> Self {
Self {
vehicle_id: vehicle_id.into(),
endpoints: endpoints.into(),
}
}
}
impl ApiValues for GetVehicleData {
fn format(&self, url: &str) -> String {
let url = url.replace("{}", &format!("{}", *self.vehicle_id));
if self.endpoints.is_empty() {
return url;
}
let endpoints = self
.endpoints
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join(";");
format!("{}?endpoints={}", url, endpoints)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct VehicleData {
pub id: VehicleId,
@ -183,9 +263,9 @@ pub struct ClimateState {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriveState {
/// gak: The following fields (up to native_type) suddenly vanished from the API response so
/// I've made them all Option. Maybe API now only returns them when driving?
/// TODO: Check if they come back
/// From https://developer.tesla.com/docs/fleet-api#vehicle_data
/// For vehicles running firmware versions 2023.38+, location_data is required to fetch vehicle
/// location. This will result in a location sharing icon to show on the vehicle UI.
pub gps_as_of: Option<i64>,
pub heading: Option<i64>,
pub latitude: Option<f64>,
@ -461,6 +541,23 @@ mod tests {
use super::*;
use crate::{PrintResponses, RequestData};
#[test]
fn vehicle_query() {
let url = "A/{}/B";
let get_vehicle_data = GetVehicleData::new(123);
assert_eq!(get_vehicle_data.format(url), "A/123/B");
let get_vehicle_data = GetVehicleData::new_with_endpoints(
123,
vec![Endpoint::ChargeState, Endpoint::ClimateState],
);
assert_eq!(
get_vehicle_data.format(url),
"A/123/B?endpoints=charge_state;climate_state"
);
}
#[test]
fn json_charge_state() {
let s = r#"