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:
parent
f0f4a61fa2
commit
6facc27d8b
|
@ -1,6 +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::vehicles::GetVehicleData;
|
||||||
use teslatte::{OwnerApi, VehicleApi};
|
use teslatte::{OwnerApi, VehicleApi};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -20,7 +21,8 @@ async fn main() {
|
||||||
dbg!(&*vehicles);
|
dbg!(&*vehicles);
|
||||||
|
|
||||||
if !vehicles.is_empty() {
|
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);
|
dbg!(&vehicle_data);
|
||||||
} else {
|
} else {
|
||||||
println!("No vehicles found!");
|
println!("No vehicles found!");
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::vehicles::{
|
use crate::vehicles::{
|
||||||
SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures,
|
Endpoints, GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging,
|
||||||
|
SetScheduledDeparture, SetTemperatures,
|
||||||
};
|
};
|
||||||
use crate::{OwnerApi, VehicleApi, VehicleId};
|
use crate::{OwnerApi, VehicleApi, VehicleId};
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
@ -7,7 +8,7 @@ use clap::{Args, Subcommand};
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum VehicleCommand {
|
pub enum VehicleCommand {
|
||||||
/// Get vehicle data.
|
/// Get vehicle data.
|
||||||
VehicleData,
|
VehicleData(Endpoints),
|
||||||
|
|
||||||
/// Open the charge port door or unlocks the cable.
|
/// Open the charge port door or unlocks the cable.
|
||||||
ChargePortDoorOpen,
|
ChargePortDoorOpen,
|
||||||
|
@ -75,8 +76,9 @@ pub struct VehicleArgs {
|
||||||
impl VehicleArgs {
|
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(endpoints) => {
|
||||||
api.vehicle_data(&self.id).await?;
|
let get_vehicle_data = GetVehicleData::new_with_endpoints(self.id, endpoints);
|
||||||
|
api.vehicle_data(&get_vehicle_data).await?;
|
||||||
}
|
}
|
||||||
VehicleCommand::SetChargeLimit(limit) => {
|
VehicleCommand::SetChargeLimit(limit) => {
|
||||||
api.set_charge_limit(&self.id, &limit).await?;
|
api.set_charge_limit(&self.id, &limit).await?;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::products::EnergySiteId;
|
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 chrono::{DateTime, FixedOffset};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use strum::{Display, EnumString, IntoStaticStr};
|
use strum::{Display, EnumString, IntoStaticStr};
|
||||||
|
@ -194,7 +194,7 @@ pub struct CalendarHistoryValues {
|
||||||
pub end_date: Option<DateTime<FixedOffset>>,
|
pub end_date: Option<DateTime<FixedOffset>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Values for CalendarHistoryValues {
|
impl ApiValues for CalendarHistoryValues {
|
||||||
fn format(&self, url: &str) -> String {
|
fn format(&self, url: &str) -> String {
|
||||||
let url = url.replace("{}", &format!("{}", self.site_id.0));
|
let url = url.replace("{}", &format!("{}", self.site_id.0));
|
||||||
let mut pairs: Vec<(&str, String)> = vec![
|
let mut pairs: Vec<(&str, String)> = vec![
|
||||||
|
|
27
src/lib.rs
27
src/lib.rs
|
@ -5,11 +5,11 @@
|
||||||
use crate::auth::{AccessToken, RefreshToken};
|
use crate::auth::{AccessToken, RefreshToken};
|
||||||
use crate::error::TeslatteError;
|
use crate::error::TeslatteError;
|
||||||
use crate::vehicles::{
|
use crate::vehicles::{
|
||||||
SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures,
|
GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture,
|
||||||
Vehicle, VehicleData,
|
SetTemperatures, Vehicle, VehicleData,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, SecondsFormat, TimeZone};
|
use chrono::{DateTime, SecondsFormat, TimeZone};
|
||||||
use derive_more::{Display, FromStr};
|
use derive_more::{Deref, Display, From, FromStr};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
|
@ -29,7 +29,10 @@ const API_URL: &str = "https://owner-api.teslamotors.com/api/1";
|
||||||
|
|
||||||
pub trait VehicleApi {
|
pub trait VehicleApi {
|
||||||
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
|
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>;
|
async fn wake_up(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
|
@ -97,16 +100,22 @@ pub trait VehicleApi {
|
||||||
|
|
||||||
trait EnergySitesApi {}
|
trait EnergySitesApi {}
|
||||||
|
|
||||||
trait Values {
|
trait ApiValues {
|
||||||
fn format(&self, url: &str) -> String;
|
fn format(&self, url: &str) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vehicle ID used by the owner-api endpoint.
|
/// Vehicle ID used by the owner-api endpoint.
|
||||||
///
|
///
|
||||||
/// This data comes from [`OwnerApi::vehicles()`] `id` field.
|
/// 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);
|
pub struct VehicleId(u64);
|
||||||
|
|
||||||
|
impl VehicleId {
|
||||||
|
pub fn new(id: u64) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Vehicle ID used by other endpoints.
|
/// Vehicle ID used by other endpoints.
|
||||||
///
|
///
|
||||||
/// This data comes from [`OwnerApi::vehicles()`] `vehicle_id` field.
|
/// 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.
|
/// 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.
|
||||||
|
#[allow(unused_macros)]
|
||||||
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) => {
|
||||||
async fn $name(
|
async fn $name(
|
||||||
|
@ -345,6 +355,7 @@ macro_rules! get_arg {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub(crate) use get_arg;
|
pub(crate) use get_arg;
|
||||||
|
|
||||||
/// Public variant of get_arg.
|
/// Public variant of get_arg.
|
||||||
|
@ -362,8 +373,7 @@ macro_rules! pub_get_arg {
|
||||||
}
|
}
|
||||||
pub(crate) use pub_get_arg;
|
pub(crate) use pub_get_arg;
|
||||||
|
|
||||||
/// GET /api/1/[url] with a struct.
|
/// GET /api/1/[url] with a struct to format the URL.
|
||||||
#[allow(unused)] // Leaving this here for now. I'm sure it'll be used during this refactor.
|
|
||||||
macro_rules! get_args {
|
macro_rules! get_args {
|
||||||
($name:ident, $return_type:ty, $url:expr, $args:ty) => {
|
($name:ident, $return_type:ty, $url:expr, $args:ty) => {
|
||||||
async fn $name(
|
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;
|
pub(crate) use get_args;
|
||||||
|
|
||||||
/// Public variant of get_args.
|
/// Public variant of get_args.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::energy_sites::{HistoryKind, HistoryPeriod};
|
use crate::energy_sites::{HistoryKind, HistoryPeriod};
|
||||||
use crate::products::GatewayId;
|
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 chrono::{DateTime, FixedOffset};
|
||||||
use derive_more::{Display, FromStr};
|
use derive_more::{Display, FromStr};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -33,7 +33,7 @@ pub struct PowerwallEnergyHistoryValues {
|
||||||
pub end_date: Option<DateTime<FixedOffset>>,
|
pub end_date: Option<DateTime<FixedOffset>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Values for PowerwallEnergyHistoryValues {
|
impl ApiValues for PowerwallEnergyHistoryValues {
|
||||||
fn format(&self, url: &str) -> String {
|
fn format(&self, url: &str) -> String {
|
||||||
let url = url.replace("{}", &self.powerwall_id.0.to_string());
|
let url = url.replace("{}", &self.powerwall_id.0.to_string());
|
||||||
let mut pairs: Vec<(&str, String)> = vec![
|
let mut pairs: Vec<(&str, String)> = vec![
|
||||||
|
|
|
@ -88,7 +88,7 @@ pub struct Components {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
|
use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
|
||||||
use crate::Values;
|
use crate::ApiValues;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
117
src/vehicles.rs
117
src/vehicles.rs
|
@ -1,17 +1,19 @@
|
||||||
/// Please note that many of these structs are generated from my own API call responses.
|
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.
|
// 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::{
|
use crate::{
|
||||||
get, get_arg, post_arg, post_arg_empty, Empty, ExternalVehicleId, OwnerApi, VehicleApi,
|
get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi,
|
||||||
VehicleId,
|
VehicleApi, VehicleId,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
impl VehicleApi for 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_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData);
|
||||||
post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId);
|
post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId);
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
|
@ -41,6 +43,84 @@ impl VehicleApi for OwnerApi {
|
||||||
post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", VehicleId);
|
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)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct VehicleData {
|
pub struct VehicleData {
|
||||||
pub id: VehicleId,
|
pub id: VehicleId,
|
||||||
|
@ -183,9 +263,9 @@ pub struct ClimateState {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DriveState {
|
pub struct DriveState {
|
||||||
/// gak: The following fields (up to native_type) suddenly vanished from the API response so
|
/// From https://developer.tesla.com/docs/fleet-api#vehicle_data
|
||||||
/// I've made them all Option. Maybe API now only returns them when driving?
|
/// For vehicles running firmware versions 2023.38+, location_data is required to fetch vehicle
|
||||||
/// TODO: Check if they come back
|
/// location. This will result in a location sharing icon to show on the vehicle UI.
|
||||||
pub gps_as_of: Option<i64>,
|
pub gps_as_of: Option<i64>,
|
||||||
pub heading: Option<i64>,
|
pub heading: Option<i64>,
|
||||||
pub latitude: Option<f64>,
|
pub latitude: Option<f64>,
|
||||||
|
@ -461,6 +541,23 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{PrintResponses, RequestData};
|
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]
|
#[test]
|
||||||
fn json_charge_state() {
|
fn json_charge_state() {
|
||||||
let s = r#"
|
let s = r#"
|
||||||
|
|
Loading…
Reference in a new issue