Compare commits
32 commits
cf30a7d1bc
...
ef26ce4697
Author | SHA1 | Date | |
---|---|---|---|
Alex Janka | ef26ce4697 | ||
Alex Janka | fd1794744b | ||
Alex Janka | 3add4893f9 | ||
Alex Janka | b13c40429f | ||
Alex Janka | 64aa35a969 | ||
Alex Janka | a2bc59fb34 | ||
Alex Janka | 9ecf21554e | ||
Alex Janka | 677d959e6d | ||
Alex Janka | 78d347f38e | ||
Alex Janka | 3123cad1a5 | ||
13f4be3478 | |||
e326d17c14 | |||
6f7fb72943 | |||
62b3343b21 | |||
19ac4b485d | |||
1e6c1d5ff3 | |||
fcc9e63930 | |||
afd833f13f | |||
6723c0ad02 | |||
8b5141376e | |||
0de5ee26b9 | |||
906ce9cf04 | |||
b7e2f94884 | |||
63015deb1b | |||
325b899504 | |||
a16ca64151 | |||
4ba1dd2c33 | |||
796c09c17a | |||
c8b45d5c3e | |||
280f5751cc | |||
c2b12df27b | |||
85e3728a1d |
46
CHANGELOG.md
46
CHANGELOG.md
|
@ -5,10 +5,46 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.15] - 2023-04-12
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Forgot a `total_pack_energy`. (#22)
|
||||||
|
|
||||||
|
## [0.1.14] - 2023-04-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `energy_left` and `total_pack_energy` are gone from the API. (#22)
|
||||||
|
- Don't rely on serde untagged to determine the product type, as any missing
|
||||||
|
fields will give an unspecified error. Instead directly check known fields
|
||||||
|
that will probably not be removed. https://github.com/serde-rs/serde/pull/2376
|
||||||
|
|
||||||
|
## [0.1.13] - 2023-01-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Vehicles list is gone from the API. Use `products` instead. (#13)
|
||||||
|
|
||||||
|
## [0.1.12] - 2023-01-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- PowerwallData new fields:
|
||||||
|
- powerwall_onboarding_settings_set
|
||||||
|
- storm_mode_enabled
|
||||||
|
- features
|
||||||
|
- warp_site_number
|
||||||
|
- go_off_grid_test_banner_enabled
|
||||||
|
- powerwall_tesla_electric_interested_in
|
||||||
|
- vpp_tour_enabled
|
||||||
|
- Components market_type is now Option<String>
|
||||||
|
- LiveWallConnector wall_connector_power is now f32
|
||||||
|
|
||||||
## [0.1.11] - 2023-11-11
|
## [0.1.11] - 2023-11-11
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `vehicle_data` now accepts `GetVehicleData` instead of `VehicleId`. (See [6facc27](https://github.com/gak/teslatte/commit/6facc27d8b408d35b98b4c6c0ad3e5df82328d2c))
|
- `vehicle_data` now accepts `GetVehicleData` instead of `VehicleId`. (See [6facc27](https://github.com/gak/teslatte/commit/6facc27d8b408d35b98b4c6c0ad3e5df82328d2c))
|
||||||
|
|
||||||
## [0.1.10] - 2023-11-11
|
## [0.1.10] - 2023-11-11
|
||||||
|
@ -16,17 +52,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- API changes for "api_version 67"
|
- API changes for "api_version 67"
|
||||||
|
|
||||||
- VehicleData new fields:
|
- VehicleData new fields:
|
||||||
|
|
||||||
- cached_data
|
- cached_data
|
||||||
- command_signing
|
- command_signing
|
||||||
- release_notes_supported
|
- release_notes_supported
|
||||||
|
|
||||||
- ClimateState new fields:
|
- ClimateState new fields:
|
||||||
|
|
||||||
- auto_steering_wheel_heat
|
- auto_steering_wheel_heat
|
||||||
- cop_activation_temperature,
|
- cop_activation_temperature,
|
||||||
- steering_wheel_heat_level
|
- steering_wheel_heat_level
|
||||||
|
|
||||||
- DriveState now Optional:
|
- DriveState now Optional:
|
||||||
|
|
||||||
- gps_as_of
|
- gps_as_of
|
||||||
- heading
|
- heading
|
||||||
- latitude
|
- latitude
|
||||||
|
@ -37,16 +77,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- native_type
|
- native_type
|
||||||
|
|
||||||
- DriveState new fields:
|
- DriveState new fields:
|
||||||
|
|
||||||
- active_route_traffic_minutes_delay
|
- active_route_traffic_minutes_delay
|
||||||
|
|
||||||
- GuiSettings new field:
|
- GuiSettings new field:
|
||||||
|
|
||||||
- gui_tirepressure_units
|
- gui_tirepressure_units
|
||||||
|
|
||||||
- VehicleConfig new fields:
|
- VehicleConfig new fields:
|
||||||
|
|
||||||
- cop_user_set_temp_supported
|
- cop_user_set_temp_supported
|
||||||
- webcam_selfie_supported
|
- webcam_selfie_supported
|
||||||
|
|
||||||
- VehicleState new fields:
|
- VehicleState new fields:
|
||||||
|
|
||||||
- media_info: MediaInfo
|
- media_info: MediaInfo
|
||||||
- tpms_hard_warning_fl
|
- tpms_hard_warning_fl
|
||||||
- tpms_hard_warning_fr
|
- tpms_hard_warning_fr
|
||||||
|
|
28
Cargo.toml
28
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "teslatte"
|
name = "teslatte"
|
||||||
version = "0.1.11"
|
version = "0.1.15"
|
||||||
description = "A command line tool and Rust crate for querying the Tesla API."
|
description = "A command line tool and Rust crate for querying the Tesla API."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75.0"
|
rust-version = "1.75.0"
|
||||||
|
@ -20,25 +20,25 @@ path = "src/main.rs"
|
||||||
required-features = ["cli"]
|
required-features = ["cli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
miette = { version = "5.10.0", features = ["fancy"] }
|
miette = { version = "7.2.0", features = ["fancy"] }
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.56"
|
||||||
tokio = { version = "1.33.0", features = ["full"] }
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
reqwest = { version = "0.11.22", features = ["rustls-tls", "cookies", "json"] }
|
reqwest = { version = "0.12.3", features = ["rustls-tls", "cookies", "json"] }
|
||||||
url = "2.4.1"
|
url = "2.5.0"
|
||||||
serde = { version = "1.0.189", features = ["derive"] }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
serde_json = "1.0.107"
|
serde_json = "1.0.111"
|
||||||
rustls = "0.22.1"
|
rustls = "0.23.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
strum = { version = "0.25.0", features = ["derive"] }
|
strum = { version = "0.26.1", features = ["derive"] }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
pkce = "0.2.0"
|
pkce = "0.2.0"
|
||||||
|
|
||||||
clap = { version = "4.4.6", features = ["derive", "env"], optional = true }
|
clap = { version = "4.4.18", features = ["derive", "env"], optional = true }
|
||||||
tracing-subscriber = { version = "0.3.17", optional = true }
|
tracing-subscriber = { version = "0.3.18", optional = true }
|
||||||
colored_json = { version = "4.0.0", optional = true }
|
colored_json = { version = "5.0.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
test-log = { version = "0.2.13", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2.14", default-features = false, features = ["trace"] }
|
||||||
|
|
|
@ -1,8 +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;
|
||||||
use teslatte::{OwnerApi, VehicleApi};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -17,19 +16,8 @@ async fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let vehicles = api.vehicles().await.unwrap();
|
|
||||||
dbg!(&*vehicles);
|
|
||||||
|
|
||||||
if !vehicles.is_empty() {
|
|
||||||
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!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let products = api.products().await.unwrap();
|
let products = api.products().await.unwrap();
|
||||||
dbg!(&*products);
|
dbg!(&products);
|
||||||
|
|
||||||
if !products.is_empty() {
|
if !products.is_empty() {
|
||||||
for product in &*products {
|
for product in &*products {
|
||||||
|
|
7
justfile
7
justfile
|
@ -7,9 +7,8 @@ no_token_test:
|
||||||
|
|
||||||
# Require an access token from "cli.json". Use `just auth` to generate.
|
# Require an access token from "cli.json". Use `just auth` to generate.
|
||||||
token_tests:
|
token_tests:
|
||||||
cargo run -- api vehicles
|
|
||||||
cargo run --no-default-features --features cli -- api vehicles
|
|
||||||
cargo run -- api products
|
cargo run -- api products
|
||||||
|
cargo run --no-default-features --features cli -- api products
|
||||||
|
|
||||||
publish version:
|
publish version:
|
||||||
git diff-index --quiet HEAD
|
git diff-index --quiet HEAD
|
||||||
|
@ -30,3 +29,7 @@ auth:
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
cargo audit
|
cargo audit
|
||||||
|
|
||||||
|
update:
|
||||||
|
cargo update && cargo upgrade
|
||||||
|
cd tesla_api_coverage && cargo update && cargo upgrade
|
||||||
|
|
65
src/auth.rs
65
src/auth.rs
|
@ -1,5 +1,5 @@
|
||||||
use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState};
|
use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState};
|
||||||
use crate::{OwnerApi, TeslatteError};
|
use crate::{FleetApi, OwnerApi, TeslatteError};
|
||||||
use derive_more::{Display, FromStr};
|
use derive_more::{Display, FromStr};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
@ -10,6 +10,7 @@ use url::Url;
|
||||||
|
|
||||||
const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize";
|
const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize";
|
||||||
const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token";
|
const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token";
|
||||||
|
const CLIENT_ID: &str = "48ad82d96e76-4cf0-a301-08794a139ad9";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
|
||||||
pub struct AccessToken(pub String);
|
pub struct AccessToken(pub String);
|
||||||
|
@ -193,6 +194,68 @@ page, where the URL will start with https://auth.tesla.com/void/callback?code=..
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FleetApi {
|
||||||
|
/// Refresh the internally stored access token using the known refresh token.
|
||||||
|
pub async fn refresh(&mut self) -> Result<(), TeslatteError> {
|
||||||
|
match &self.refresh_token {
|
||||||
|
None => Err(TeslatteError::NoRefreshToken),
|
||||||
|
Some(refresh_token) => {
|
||||||
|
let response = Self::refresh_token(refresh_token).await?;
|
||||||
|
self.access_token = response.access_token;
|
||||||
|
self.refresh_token = Some(response.refresh_token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_token(
|
||||||
|
refresh_token: &RefreshToken,
|
||||||
|
) -> Result<RefreshTokenResponse, TeslatteError> {
|
||||||
|
let url = "https://auth.tesla.com/oauth2/v3/token";
|
||||||
|
let payload = RefreshTokenRequest {
|
||||||
|
grant_type: "refresh_token".into(),
|
||||||
|
client_id: CLIENT_ID.into(),
|
||||||
|
refresh_token: refresh_token.0.clone(),
|
||||||
|
scope: "openid email offline_access".into(),
|
||||||
|
};
|
||||||
|
Self::auth_post(url, &payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_post<'a, S, D>(url: &str, payload: &S) -> Result<D, TeslatteError>
|
||||||
|
where
|
||||||
|
S: Serialize,
|
||||||
|
D: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let response = Client::new()
|
||||||
|
.post(url)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.json(payload)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|source| TeslatteError::FetchError {
|
||||||
|
source,
|
||||||
|
request: url.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|source| TeslatteError::FetchError {
|
||||||
|
source,
|
||||||
|
request: url.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let json =
|
||||||
|
serde_json::from_str::<D>(&body).map_err(|source| TeslatteError::DecodeJsonError {
|
||||||
|
source,
|
||||||
|
body: body.to_string(),
|
||||||
|
request: url.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct RefreshTokenRequest {
|
struct RefreshTokenRequest {
|
||||||
grant_type: String,
|
grant_type: String,
|
||||||
|
|
|
@ -18,7 +18,6 @@ pub struct SiteStatus {
|
||||||
pub battery_power: i64,
|
pub battery_power: i64,
|
||||||
pub battery_type: String,
|
pub battery_type: String,
|
||||||
pub breaker_alert_enabled: bool,
|
pub breaker_alert_enabled: bool,
|
||||||
pub energy_left: f64,
|
|
||||||
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,
|
||||||
|
@ -29,14 +28,12 @@ pub struct SiteStatus {
|
||||||
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,
|
||||||
pub total_pack_energy: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
pub struct LiveStatus {
|
pub struct LiveStatus {
|
||||||
pub backup_capable: bool,
|
pub backup_capable: bool,
|
||||||
pub battery_power: i64,
|
pub battery_power: i64,
|
||||||
pub energy_left: f64,
|
|
||||||
pub generator_power: i64,
|
pub generator_power: i64,
|
||||||
pub grid_power: i64,
|
pub grid_power: i64,
|
||||||
pub grid_services_active: bool,
|
pub grid_services_active: bool,
|
||||||
|
@ -48,7 +45,6 @@ pub struct LiveStatus {
|
||||||
pub solar_power: i64,
|
pub solar_power: i64,
|
||||||
pub storm_mode_active: bool,
|
pub storm_mode_active: bool,
|
||||||
pub timestamp: String,
|
pub timestamp: String,
|
||||||
pub total_pack_energy: i64,
|
|
||||||
pub wall_connectors: Vec<LiveWallConnector>,
|
pub wall_connectors: Vec<LiveWallConnector>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +65,7 @@ pub struct LiveWallConnector {
|
||||||
/// 2: not plugged in
|
/// 2: not plugged in
|
||||||
/// 4: plugged in (not charging)
|
/// 4: plugged in (not charging)
|
||||||
pub wall_connector_state: i64,
|
pub wall_connector_state: i64,
|
||||||
pub wall_connector_power: i64,
|
pub wall_connector_power: f32,
|
||||||
pub wall_connector_fault_state: i64,
|
pub wall_connector_fault_state: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +307,7 @@ mod tests {
|
||||||
vin: Some("1234".to_string()),
|
vin: Some("1234".to_string()),
|
||||||
din: "5432".to_string(),
|
din: "5432".to_string(),
|
||||||
wall_connector_state: 4,
|
wall_connector_state: 4,
|
||||||
wall_connector_power: 0,
|
wall_connector_power: 0.,
|
||||||
wall_connector_fault_state: 2,
|
wall_connector_fault_state: 2,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -336,7 +332,7 @@ mod tests {
|
||||||
vin: None,
|
vin: None,
|
||||||
din: "1234".to_string(),
|
din: "1234".to_string(),
|
||||||
wall_connector_state: 2,
|
wall_connector_state: 2,
|
||||||
wall_connector_power: 0,
|
wall_connector_power: 0.,
|
||||||
wall_connector_fault_state: 2,
|
wall_connector_fault_state: 2,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
195
src/lib.rs
195
src/lib.rs
|
@ -25,10 +25,13 @@ pub mod vehicles;
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
|
||||||
const API_URL: &str = "https://owner-api.teslamotors.com/api/1";
|
const API_URL: &str = if cfg!(debug_assertions) {
|
||||||
|
"http://cnut.internal.alexjanka.com:4444/api/1"
|
||||||
|
} else {
|
||||||
|
"https://localhost:4443/api/1"
|
||||||
|
};
|
||||||
|
|
||||||
pub trait VehicleApi {
|
pub trait VehicleApi {
|
||||||
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
|
|
||||||
async fn vehicle_data(
|
async fn vehicle_data(
|
||||||
&self,
|
&self,
|
||||||
get_vehicle_data: &GetVehicleData,
|
get_vehicle_data: &GetVehicleData,
|
||||||
|
@ -98,7 +101,60 @@ pub trait VehicleApi {
|
||||||
) -> Result<PostResponse, TeslatteError>;
|
) -> Result<PostResponse, TeslatteError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
trait EnergySitesApi {}
|
pub trait FleetVehicleApi {
|
||||||
|
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
|
||||||
|
async fn vehicle_data(
|
||||||
|
&self,
|
||||||
|
get_vehicle_data: &GetVehicleData,
|
||||||
|
) -> Result<VehicleData, TeslatteError>;
|
||||||
|
async fn wake_up(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
async fn honk_horn(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn flash_lights(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
|
||||||
|
// Charging
|
||||||
|
async fn charge_port_door_open(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn charge_port_door_close(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn set_charge_limit(
|
||||||
|
&self,
|
||||||
|
vin: &str,
|
||||||
|
data: &SetChargeLimit,
|
||||||
|
) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn set_charging_amps(
|
||||||
|
&self,
|
||||||
|
vin: &str,
|
||||||
|
data: &SetChargingAmps,
|
||||||
|
) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn charge_standard(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn charge_max_range(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn charge_start(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn charge_stop(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn set_scheduled_charging(
|
||||||
|
&self,
|
||||||
|
vin: &str,
|
||||||
|
data: &SetScheduledCharging,
|
||||||
|
) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn set_scheduled_departure(
|
||||||
|
&self,
|
||||||
|
vin: &str,
|
||||||
|
data: &SetScheduledDeparture,
|
||||||
|
) -> Result<PostResponse, TeslatteError>;
|
||||||
|
|
||||||
|
// HVAC
|
||||||
|
async fn auto_conditioning_start(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn auto_conditioning_stop(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn set_temps(
|
||||||
|
&self,
|
||||||
|
vin: &str,
|
||||||
|
data: &SetTemperatures,
|
||||||
|
) -> Result<PostResponse, TeslatteError>;
|
||||||
|
|
||||||
|
// Doors
|
||||||
|
async fn door_unlock(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn door_lock(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
async fn remote_start_drive(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
|
||||||
|
}
|
||||||
|
|
||||||
trait ApiValues {
|
trait ApiValues {
|
||||||
fn format(&self, url: &str) -> String;
|
fn format(&self, url: &str) -> String;
|
||||||
|
@ -107,7 +163,7 @@ trait ApiValues {
|
||||||
/// 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, From, Deref)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Display, FromStr, From, Deref)]
|
||||||
pub struct VehicleId(u64);
|
pub struct VehicleId(u64);
|
||||||
|
|
||||||
impl VehicleId {
|
impl VehicleId {
|
||||||
|
@ -272,6 +328,135 @@ impl OwnerApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FleetApi {
|
||||||
|
pub access_token: AccessToken,
|
||||||
|
pub refresh_token: Option<RefreshToken>,
|
||||||
|
pub print_responses: PrintResponses,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FleetApi {
|
||||||
|
pub fn new(access_token: AccessToken, refresh_token: Option<RefreshToken>) -> Self {
|
||||||
|
Self {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
print_responses: PrintResponses::No,
|
||||||
|
client: Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.add_root_certificate(
|
||||||
|
reqwest::Certificate::from_pem(include_bytes!("./selfsigned.pem")).unwrap(),
|
||||||
|
)
|
||||||
|
.danger_accept_invalid_certs(cfg!(debug_assertions))
|
||||||
|
.build()
|
||||||
|
.unwrap(), // TODO: unwrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get<D>(&self, url: &str) -> Result<D, TeslatteError>
|
||||||
|
where
|
||||||
|
D: for<'de> Deserialize<'de> + Debug,
|
||||||
|
{
|
||||||
|
self.request(&RequestData::Get { url }).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post<S>(&self, url: &str, body: S) -> Result<PostResponse, TeslatteError>
|
||||||
|
where
|
||||||
|
S: Serialize + Debug,
|
||||||
|
{
|
||||||
|
let payload =
|
||||||
|
&serde_json::to_string(&body).expect("Should not fail creating the request struct.");
|
||||||
|
let request_data = RequestData::Post { url, payload };
|
||||||
|
let data = self.request::<PostResponse>(&request_data).await?;
|
||||||
|
|
||||||
|
if !data.result {
|
||||||
|
return Err(TeslatteError::ServerError {
|
||||||
|
request: format!("{request_data}"),
|
||||||
|
description: None,
|
||||||
|
msg: data.reason,
|
||||||
|
body: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request<T>(&self, request_data: &RequestData<'_>) -> Result<T, TeslatteError>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de> + Debug,
|
||||||
|
{
|
||||||
|
debug!("{request_data}");
|
||||||
|
|
||||||
|
let request_builder = match request_data {
|
||||||
|
RequestData::Get { url } => self.client.get(*url),
|
||||||
|
RequestData::Post { url, payload } => self
|
||||||
|
.client
|
||||||
|
.post(*url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(payload.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_body = request_builder
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!("Bearer {}", self.access_token.0.trim()),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|source| TeslatteError::FetchError {
|
||||||
|
source,
|
||||||
|
request: format!("{request_data}"),
|
||||||
|
})?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|source| TeslatteError::FetchError {
|
||||||
|
source,
|
||||||
|
request: format!("{request_data}"),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
debug!("Response: {response_body}");
|
||||||
|
|
||||||
|
Self::parse_json(request_data, response_body, self.print_responses)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_json<T>(
|
||||||
|
request_data: &RequestData,
|
||||||
|
response_body: String,
|
||||||
|
print_response: PrintResponses,
|
||||||
|
) -> Result<T, TeslatteError>
|
||||||
|
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<T> = serde_json::from_str::<ResponseDeserializer<T>>(&response_body)
|
||||||
|
.map_err(|source| TeslatteError::DecodeJsonError {
|
||||||
|
source,
|
||||||
|
request: format!("{request_data}"),
|
||||||
|
body: response_body.to_string(),
|
||||||
|
})?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Response::Response(data) => Ok(data),
|
||||||
|
Response::Error(e) => Err(TeslatteError::ServerError {
|
||||||
|
request: format!("{request_data}"),
|
||||||
|
msg: e.error,
|
||||||
|
description: e.error_description,
|
||||||
|
body: Some(response_body.to_owned()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ResponseDeserializer<T> {
|
struct ResponseDeserializer<T> {
|
||||||
error: Option<ResponseError>,
|
error: Option<ResponseError>,
|
||||||
|
@ -314,6 +499,7 @@ struct ResponseError {
|
||||||
struct Empty {}
|
struct Empty {}
|
||||||
|
|
||||||
/// GET /api/1/[url]
|
/// GET /api/1/[url]
|
||||||
|
#[allow(unused_macros)]
|
||||||
macro_rules! get {
|
macro_rules! get {
|
||||||
($name:ident, $return_type:ty, $url:expr) => {
|
($name:ident, $return_type:ty, $url:expr) => {
|
||||||
async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> {
|
async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> {
|
||||||
|
@ -324,6 +510,7 @@ macro_rules! get {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub(crate) use get;
|
pub(crate) use get;
|
||||||
|
|
||||||
/// Same as get, but public.
|
/// Same as get, but public.
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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::vehicle::VehicleArgs;
|
use teslatte::cli::vehicle::VehicleArgs;
|
||||||
use teslatte::{OwnerApi, PrintResponses, VehicleApi};
|
use teslatte::{OwnerApi, PrintResponses};
|
||||||
|
|
||||||
/// Teslatte
|
/// Teslatte
|
||||||
///
|
///
|
||||||
|
@ -106,7 +106,7 @@ async fn main() -> miette::Result<()> {
|
||||||
api.print_responses = PrintResponses::Pretty;
|
api.print_responses = PrintResponses::Pretty;
|
||||||
match api_args.command {
|
match api_args.command {
|
||||||
ApiCommand::Vehicles => {
|
ApiCommand::Vehicles => {
|
||||||
api.vehicles().await?;
|
panic!("Tesla API has changed. This command is no longer works with the Owners API.");
|
||||||
}
|
}
|
||||||
ApiCommand::Vehicle(v) => {
|
ApiCommand::Vehicle(v) => {
|
||||||
v.run(&api).await?;
|
v.run(&api).await?;
|
||||||
|
|
130
src/products.rs
130
src/products.rs
|
@ -2,9 +2,10 @@ use crate::energy_sites::WallConnector;
|
||||||
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::{pub_get, OwnerApi};
|
use crate::{pub_get, FleetApi, OwnerApi};
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
@ -12,6 +13,11 @@ impl OwnerApi {
|
||||||
pub_get!(products, Vec<Product>, "/products");
|
pub_get!(products, Vec<Product>, "/products");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl FleetApi {
|
||||||
|
pub_get!(products, Vec<Product>, "/products");
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Display)]
|
#[derive(Debug, Clone, Deserialize, Display)]
|
||||||
pub struct EnergySiteId(pub u64);
|
pub struct EnergySiteId(pub u64);
|
||||||
|
|
||||||
|
@ -28,14 +34,44 @@ impl FromStr for EnergySiteId {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct GatewayId(String);
|
pub struct GatewayId(String);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone)]
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum Product {
|
pub enum Product {
|
||||||
Vehicle(Box<VehicleData>),
|
Vehicle(Box<VehicleData>),
|
||||||
Solar(Box<SolarData>),
|
Solar(Box<SolarData>),
|
||||||
Powerwall(Box<PowerwallData>),
|
Powerwall(Box<PowerwallData>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_product<'de, D>(deserializer: D) -> Result<Product, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let v = Value::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
if v.get("vehicle_id").is_some() {
|
||||||
|
let vehicle_data = VehicleData::deserialize(v).map_err(serde::de::Error::custom)?;
|
||||||
|
Ok(Product::Vehicle(Box::new(vehicle_data)))
|
||||||
|
} else if v.get("solar_type").is_some() {
|
||||||
|
let solar_data = SolarData::deserialize(v).map_err(serde::de::Error::custom)?;
|
||||||
|
Ok(Product::Solar(Box::new(solar_data)))
|
||||||
|
} else if v.get("battery_type").is_some() {
|
||||||
|
let powerwall_data = PowerwallData::deserialize(v).map_err(serde::de::Error::custom)?;
|
||||||
|
Ok(Product::Powerwall(Box::new(powerwall_data)))
|
||||||
|
} else {
|
||||||
|
Err(serde::de::Error::custom(
|
||||||
|
"No valid key found to determine the product type",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Product {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Product, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserialize_product(deserializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This is assumed from https://tesla-api.timdorr.com/api-basics/products
|
/// This is assumed from https://tesla-api.timdorr.com/api-basics/products
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct SolarData {
|
pub struct SolarData {
|
||||||
|
@ -61,14 +97,25 @@ pub struct PowerwallData {
|
||||||
pub id: PowerwallId,
|
pub id: PowerwallId,
|
||||||
pub gateway_id: GatewayId,
|
pub gateway_id: GatewayId,
|
||||||
pub asset_site_id: String,
|
pub asset_site_id: String,
|
||||||
pub energy_left: f64,
|
|
||||||
pub total_pack_energy: i64,
|
|
||||||
pub percentage_charged: f64,
|
pub percentage_charged: f64,
|
||||||
pub backup_capable: bool,
|
pub backup_capable: bool,
|
||||||
pub battery_power: i64,
|
pub battery_power: i64,
|
||||||
pub sync_grid_alert_enabled: bool,
|
pub sync_grid_alert_enabled: bool,
|
||||||
pub breaker_alert_enabled: bool,
|
pub breaker_alert_enabled: bool,
|
||||||
pub components: Components,
|
pub components: Components,
|
||||||
|
// New fields as of 2024-01-20
|
||||||
|
pub powerwall_onboarding_settings_set: bool,
|
||||||
|
pub storm_mode_enabled: bool,
|
||||||
|
pub features: PowerwallFeatures,
|
||||||
|
pub warp_site_number: String,
|
||||||
|
pub go_off_grid_test_banner_enabled: Option<bool>,
|
||||||
|
pub powerwall_tesla_electric_interested_in: Option<bool>,
|
||||||
|
pub vpp_tour_enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct PowerwallFeatures {
|
||||||
|
pub rate_plan_manager_no_pricing_constraint: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -79,7 +126,7 @@ pub struct Components {
|
||||||
pub solar_type: Option<String>,
|
pub solar_type: Option<String>,
|
||||||
pub grid: bool,
|
pub grid: bool,
|
||||||
pub load_meter: bool,
|
pub load_meter: bool,
|
||||||
pub market_type: String,
|
pub market_type: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub wall_connectors: Vec<WallConnector>,
|
pub wall_connectors: Vec<WallConnector>,
|
||||||
}
|
}
|
||||||
|
@ -88,60 +135,9 @@ 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::ApiValues;
|
use crate::{ApiValues, PrintResponses, RequestData};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn energy_match_powerwall() {
|
|
||||||
let json = r#"
|
|
||||||
{
|
|
||||||
"energy_site_id": 1032748243,
|
|
||||||
"resource_type": "battery",
|
|
||||||
"site_name": "1 Railway Pde",
|
|
||||||
"id": "ABC2010-1234",
|
|
||||||
"gateway_id": "3287423824-QWE",
|
|
||||||
"asset_site_id": "123ecd-123ecd-12345-12345",
|
|
||||||
"energy_left": 4394.000000000001,
|
|
||||||
"total_pack_energy": 13494,
|
|
||||||
"percentage_charged": 32.562620423892106,
|
|
||||||
"battery_type": "ac_powerwall",
|
|
||||||
"backup_capable": true,
|
|
||||||
"battery_power": -280,
|
|
||||||
"sync_grid_alert_enabled": true,
|
|
||||||
"breaker_alert_enabled": false,
|
|
||||||
"components": {
|
|
||||||
"battery": true,
|
|
||||||
"battery_type": "ac_powerwall",
|
|
||||||
"solar": true,
|
|
||||||
"solar_type": "pv_panel",
|
|
||||||
"grid": true,
|
|
||||||
"load_meter": true,
|
|
||||||
"market_type": "residential"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
if let Product::Powerwall(data) = serde_json::from_str(json).unwrap() {
|
|
||||||
assert_eq!(data.battery_type, "ac_powerwall");
|
|
||||||
assert!(data.backup_capable);
|
|
||||||
assert_eq!(data.battery_power, -280);
|
|
||||||
assert!(data.sync_grid_alert_enabled);
|
|
||||||
assert!(!data.breaker_alert_enabled);
|
|
||||||
assert!(data.components.battery);
|
|
||||||
assert_eq!(
|
|
||||||
data.components.battery_type,
|
|
||||||
Some("ac_powerwall".to_string())
|
|
||||||
);
|
|
||||||
assert!(data.components.solar);
|
|
||||||
assert_eq!(data.components.solar_type, Some("pv_panel".to_string()));
|
|
||||||
assert!(data.components.grid);
|
|
||||||
assert!(data.components.load_meter);
|
|
||||||
assert_eq!(data.components.market_type, "residential");
|
|
||||||
} else {
|
|
||||||
panic!("Expected PowerwallData");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn energy_match_vehicle() {
|
fn energy_match_vehicle() {
|
||||||
let json = r#"
|
let json = r#"
|
||||||
|
@ -269,4 +265,20 @@ mod tests {
|
||||||
"https://base.com/e/123/history?period=month&kind=energy&start_date=2020-01-01T00:00:00Z&end_date=2020-01-31T23:59:59Z"
|
"https://base.com/e/123/history?period=month&kind=energy&start_date=2020-01-01T00:00:00Z&end_date=2020-01-31T23:59:59Z"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_products_gak_2024_01_20() {
|
||||||
|
let s = include_str!("../testdata/products_gak_2024_01_20.json");
|
||||||
|
let request_data = RequestData::Get { url: "" };
|
||||||
|
OwnerApi::parse_json::<Vec<Product>>(&request_data, s.to_string(), PrintResponses::Pretty)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_products_gak_2024_04_12() {
|
||||||
|
let s = include_str!("../testdata/products_gak_2024_04_12.json");
|
||||||
|
let request_data = RequestData::Get { url: "" };
|
||||||
|
OwnerApi::parse_json::<Vec<Product>>(&request_data, s.to_string(), PrintResponses::Pretty)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/selfsigned.pem
Normal file
14
src/selfsigned.pem
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICJjCCAYigAwIBAgIUT9V89Ca28OFWbQM4tx7rBOaqw4UwCgYIKoZIzj0EAwIw
|
||||||
|
FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMTIyNzA3MDcxNVoXDTMzMTIyNDA3
|
||||||
|
MDcxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIGbMBAGByqGSM49AgEGBSuBBAAj
|
||||||
|
A4GGAAQAQ0BavfhlRoxnE4CmRx22LKafnx6Oqs0fO/vSmPqgqWzMmMUCsak9bNoJ
|
||||||
|
fNSFS2beUY8+myR2kpAPadp5AQAz2wsBCHEriqlp88sQKJWGaAzzmuZP0UmxaIJK
|
||||||
|
Ftcv0RLyuWST5NN61xp0yzrbF9tSjXq34qrIPcxU7t2t3IylP3rApYujdTBzMB0G
|
||||||
|
A1UdDgQWBBThM5D2TLjTuZ6we4CgyRl+iWScezAfBgNVHSMEGDAWgBThM5D2TLjT
|
||||||
|
uZ6we4CgyRl+iWScezAPBgNVHRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUF
|
||||||
|
BwMBMAsGA1UdDwQEAwICjDAKBggqhkjOPQQDAgOBiwAwgYcCQUx062mqiK+K8AFf
|
||||||
|
/TkjzxXYacUbvy0+ubIpytOMOT36noMkShe8m0Y/1Y3l2HGlSvWeTQzkCF0fIu/d
|
||||||
|
NdBiRFkwAkIBtqfzcXGHklZgKNg9iKfUhoX93mDUFv/b1Z3AGHYHnVT3kJvOy2zO
|
||||||
|
uQLK0NgYXCVMADGzjWuY14XYvTyTBGxfw2w=
|
||||||
|
-----END CERTIFICATE-----
|
113
src/vehicles.rs
113
src/vehicles.rs
|
@ -4,15 +4,14 @@ use derive_more::{Deref, DerefMut, From};
|
||||||
// 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_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi,
|
get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, FleetApi,
|
||||||
VehicleApi, VehicleId,
|
FleetVehicleApi, OwnerApi, VehicleApi, VehicleId,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
impl VehicleApi for OwnerApi {
|
impl VehicleApi for OwnerApi {
|
||||||
get!(vehicles, Vec<Vehicle>, "/vehicles");
|
|
||||||
get_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData);
|
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);
|
||||||
|
|
||||||
|
@ -43,6 +42,39 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl FleetVehicleApi for FleetApi {
|
||||||
|
get!(vehicles, Vec<Vehicle>, "/vehicles");
|
||||||
|
get_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData);
|
||||||
|
post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", str);
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
post_arg_empty!(honk_horn, "/vehicles/{}/command/honk_horn", str);
|
||||||
|
post_arg_empty!(flash_lights, "/vehicles/{}/command/flash_lights", str);
|
||||||
|
|
||||||
|
// Charging
|
||||||
|
post_arg_empty!(charge_port_door_open, "/vehicles/{}/command/charge_port_door_open", str);
|
||||||
|
post_arg_empty!(charge_port_door_close, "/vehicles/{}/command/charge_port_door_close", str);
|
||||||
|
post_arg!(set_charge_limit, SetChargeLimit, "/vehicles/{}/command/set_charge_limit", str);
|
||||||
|
post_arg!(set_charging_amps, SetChargingAmps, "/vehicles/{}/command/set_charging_amps", str);
|
||||||
|
post_arg_empty!(charge_standard, "/vehicles/{}/command/charge_standard", str);
|
||||||
|
post_arg_empty!(charge_max_range, "/vehicles/{}/command/charge_max_range", str);
|
||||||
|
post_arg_empty!(charge_start, "/vehicles/{}/command/charge_start", str);
|
||||||
|
post_arg_empty!(charge_stop, "/vehicles/{}/command/charge_stop", str);
|
||||||
|
post_arg!(set_scheduled_charging, SetScheduledCharging, "/vehicles/{}/command/set_scheduled_charging", str);
|
||||||
|
post_arg!(set_scheduled_departure, SetScheduledDeparture, "/vehicles/{}/command/set_scheduled_departure", str);
|
||||||
|
|
||||||
|
// HVAC
|
||||||
|
post_arg_empty!(auto_conditioning_start, "/vehicles/{}/command/auto_conditioning_start", str);
|
||||||
|
post_arg_empty!(auto_conditioning_stop, "/vehicles/{}/command/auto_conditioning_stop", str);
|
||||||
|
post_arg!(set_temps, SetTemperatures, "/vehicles/{}/command/set_temps", str);
|
||||||
|
|
||||||
|
// Doors
|
||||||
|
post_arg_empty!(door_unlock, "/vehicles/{}/command/door_unlock", str);
|
||||||
|
post_arg_empty!(door_lock, "/vehicles/{}/command/door_lock", str);
|
||||||
|
post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", str);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, EnumString)]
|
#[derive(Debug, Clone, Display, EnumString)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum Endpoint {
|
pub enum Endpoint {
|
||||||
|
@ -185,7 +217,7 @@ pub struct ChargeState {
|
||||||
pub charger_pilot_current: Option<i64>,
|
pub charger_pilot_current: Option<i64>,
|
||||||
pub charger_power: Option<i64>,
|
pub charger_power: Option<i64>,
|
||||||
pub charger_voltage: Option<i64>,
|
pub charger_voltage: Option<i64>,
|
||||||
pub charging_state: String,
|
pub charging_state: ChargingState,
|
||||||
pub conn_charge_cable: String,
|
pub conn_charge_cable: String,
|
||||||
pub est_battery_range: f64,
|
pub est_battery_range: f64,
|
||||||
pub fast_charger_brand: String,
|
pub fast_charger_brand: String,
|
||||||
|
@ -208,8 +240,8 @@ pub struct ChargeState {
|
||||||
pub scheduled_charging_start_time: Option<i64>,
|
pub scheduled_charging_start_time: Option<i64>,
|
||||||
pub scheduled_charging_start_time_app: Option<i64>,
|
pub scheduled_charging_start_time_app: Option<i64>,
|
||||||
pub scheduled_charging_start_time_minutes: Option<i64>,
|
pub scheduled_charging_start_time_minutes: Option<i64>,
|
||||||
pub scheduled_departure_time: i64,
|
pub scheduled_departure_time: Option<i64>,
|
||||||
pub scheduled_departure_time_minutes: i64,
|
pub scheduled_departure_time_minutes: Option<i64>,
|
||||||
pub supercharger_session_trip_planner: bool,
|
pub supercharger_session_trip_planner: bool,
|
||||||
pub time_to_full_charge: f64,
|
pub time_to_full_charge: f64,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
|
@ -218,6 +250,16 @@ pub struct ChargeState {
|
||||||
pub user_charge_enable_request: Option<bool>,
|
pub user_charge_enable_request: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum ChargingState {
|
||||||
|
Charging,
|
||||||
|
Stopped,
|
||||||
|
Disconnected,
|
||||||
|
Complete,
|
||||||
|
#[serde(other)]
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ClimateState {
|
pub struct ClimateState {
|
||||||
pub allow_cabin_overheat_protection: bool,
|
pub allow_cabin_overheat_protection: bool,
|
||||||
|
@ -225,13 +267,13 @@ pub struct ClimateState {
|
||||||
pub auto_seat_climate_right: Option<bool>,
|
pub auto_seat_climate_right: Option<bool>,
|
||||||
pub battery_heater: bool,
|
pub battery_heater: bool,
|
||||||
pub battery_heater_no_power: Option<bool>,
|
pub battery_heater_no_power: Option<bool>,
|
||||||
pub cabin_overheat_protection: String,
|
pub cabin_overheat_protection: CabinOverheatProtection,
|
||||||
pub cabin_overheat_protection_actively_cooling: Option<bool>,
|
pub cabin_overheat_protection_actively_cooling: Option<bool>,
|
||||||
pub climate_keeper_mode: String,
|
pub climate_keeper_mode: String,
|
||||||
pub defrost_mode: i64,
|
pub defrost_mode: i64,
|
||||||
pub driver_temp_setting: f64,
|
pub driver_temp_setting: f64,
|
||||||
pub fan_status: i64,
|
pub fan_status: i64,
|
||||||
pub hvac_auto_request: String,
|
pub hvac_auto_request: HvacAutoRequest,
|
||||||
pub inside_temp: Option<f64>,
|
pub inside_temp: Option<f64>,
|
||||||
pub is_auto_conditioning_on: Option<bool>,
|
pub is_auto_conditioning_on: Option<bool>,
|
||||||
pub is_climate_on: bool,
|
pub is_climate_on: bool,
|
||||||
|
@ -261,6 +303,23 @@ pub struct ClimateState {
|
||||||
pub steering_wheel_heat_level: Option<i64>,
|
pub steering_wheel_heat_level: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum CabinOverheatProtection {
|
||||||
|
Off,
|
||||||
|
On,
|
||||||
|
FanOnly,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum HvacAutoRequest {
|
||||||
|
Override,
|
||||||
|
On,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DriveState {
|
pub struct DriveState {
|
||||||
/// From https://developer.tesla.com/docs/fleet-api#vehicle_data
|
/// From https://developer.tesla.com/docs/fleet-api#vehicle_data
|
||||||
|
@ -276,14 +335,42 @@ pub struct DriveState {
|
||||||
pub native_type: Option<String>,
|
pub native_type: Option<String>,
|
||||||
|
|
||||||
pub power: i64,
|
pub power: i64,
|
||||||
pub shift_state: Option<String>,
|
#[serde(deserialize_with = "map_null_to_default")]
|
||||||
|
pub shift_state: ShiftState,
|
||||||
/// gak: I've assumed this to be String.
|
/// gak: I've assumed this to be String.
|
||||||
pub speed: Option<String>,
|
pub speed: Option<i64>,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
|
|
||||||
pub active_route_traffic_minutes_delay: Option<f64>,
|
pub active_route_traffic_minutes_delay: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum ShiftState {
|
||||||
|
#[serde(alias = "D")]
|
||||||
|
Drive,
|
||||||
|
#[serde(alias = "N")]
|
||||||
|
Neutral,
|
||||||
|
#[serde(alias = "R")]
|
||||||
|
Reverse,
|
||||||
|
#[serde(other)]
|
||||||
|
#[serde(alias = "P")]
|
||||||
|
Park,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShiftState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Park
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_null_to_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: Default + Deserialize<'de>,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct GuiSettings {
|
pub struct GuiSettings {
|
||||||
pub gui_24_hour_time: bool,
|
pub gui_24_hour_time: bool,
|
||||||
|
@ -458,9 +545,7 @@ pub struct GranularAccess {
|
||||||
pub hide_private: bool,
|
pub hide_private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[allow(dead_code)]
|
||||||
pub struct Vehicles(Vec<Vehicle>);
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Vehicle {
|
pub struct Vehicle {
|
||||||
pub id: VehicleId,
|
pub id: VehicleId,
|
||||||
|
|
|
@ -4,18 +4,18 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = "0.11.22"
|
reqwest = "0.11.23"
|
||||||
tokio = { version = "1.33.0", features = ["full"] }
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
clap = { version = "4.4.6", features = ["derive"] }
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
scraper = "0.17.1"
|
scraper = "0.18.1"
|
||||||
strum = { version = "0.25.0", features = ["derive"] }
|
strum = { version = "0.25.0", features = ["derive"] }
|
||||||
tracing-subscriber = "0.3.17"
|
tracing-subscriber = "0.3.18"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.79"
|
||||||
serde = { version = "1.0.189", features = ["derive"] }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
serde_json = "1.0.107"
|
serde_json = "1.0.111"
|
||||||
heck = "0.5.0-rc.1"
|
heck = "0.5.0-rc.1"
|
||||||
glob = "0.3.1"
|
glob = "0.3.1"
|
||||||
syn = { version = "2.0.38" , features = ["full", "extra-traits"] }
|
syn = { version = "2.0.48" , features = ["full", "extra-traits"] }
|
||||||
|
|
118
testdata/products_gak_2024_01_20.json
vendored
Normal file
118
testdata/products_gak_2024_01_20.json
vendored
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"access_type": "OWNER",
|
||||||
|
"api_version": 71,
|
||||||
|
"backseat_token": null,
|
||||||
|
"backseat_token_updated_at": null,
|
||||||
|
"ble_autopair_enrolled": false,
|
||||||
|
"cached_data": "",
|
||||||
|
"calendar_enabled": true,
|
||||||
|
"color": null,
|
||||||
|
"command_signing": "required",
|
||||||
|
"display_name": "Kool Beans",
|
||||||
|
"granular_access": {
|
||||||
|
"hide_private": false
|
||||||
|
},
|
||||||
|
"id": 1,
|
||||||
|
"id_s": "1",
|
||||||
|
"in_service": false,
|
||||||
|
"option_codes": null,
|
||||||
|
"release_notes_supported": true,
|
||||||
|
"state": "online",
|
||||||
|
"tokens": [
|
||||||
|
],
|
||||||
|
"user_id": 2,
|
||||||
|
"vehicle_config": {
|
||||||
|
"aux_park_lamps": "Eu",
|
||||||
|
"badge_version": 0,
|
||||||
|
"can_accept_navigation_requests": true,
|
||||||
|
"can_actuate_trunks": true,
|
||||||
|
"car_special_type": "base",
|
||||||
|
"car_type": "model3",
|
||||||
|
"charge_port_type": "CCS",
|
||||||
|
"cop_user_set_temp_supported": false,
|
||||||
|
"dashcam_clip_save_supported": true,
|
||||||
|
"default_charge_to_max": false,
|
||||||
|
"driver_assist": "TeslaAP3",
|
||||||
|
"ece_restrictions": false,
|
||||||
|
"efficiency_package": "M32021",
|
||||||
|
"eu_vehicle": true,
|
||||||
|
"exterior_color": "MidnightSilver",
|
||||||
|
"exterior_trim": "Black",
|
||||||
|
"exterior_trim_override": "",
|
||||||
|
"has_air_suspension": false,
|
||||||
|
"has_ludicrous_mode": false,
|
||||||
|
"has_seat_cooling": false,
|
||||||
|
"headlamp_type": "Global",
|
||||||
|
"interior_trim_type": "Black2",
|
||||||
|
"key_version": 2,
|
||||||
|
"motorized_charge_port": true,
|
||||||
|
"paint_color_override": "24,24,27,0.9,0.3",
|
||||||
|
"performance_package": "Base",
|
||||||
|
"plg": true,
|
||||||
|
"pws": false,
|
||||||
|
"rear_drive_unit": "PM216MOSFET",
|
||||||
|
"rear_seat_heaters": 1,
|
||||||
|
"rear_seat_type": 0,
|
||||||
|
"rhd": true,
|
||||||
|
"roof_color": "RoofColorGlass",
|
||||||
|
"seat_type": null,
|
||||||
|
"spoiler_type": "None",
|
||||||
|
"sun_roof_installed": null,
|
||||||
|
"supports_qr_pairing": false,
|
||||||
|
"third_row_seats": "None",
|
||||||
|
"timestamp": 1705717676876,
|
||||||
|
"trim_badging": "74d",
|
||||||
|
"use_range_badging": true,
|
||||||
|
"utc_offset": 36000,
|
||||||
|
"webcam_selfie_supported": true,
|
||||||
|
"webcam_supported": true,
|
||||||
|
"wheel_type": "StilettoRefresh19"
|
||||||
|
},
|
||||||
|
"vehicle_id": 1,
|
||||||
|
"vin": "LR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset_site_id": "",
|
||||||
|
"backup_capable": true,
|
||||||
|
"battery_power": 3520,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"breaker_alert_enabled": true,
|
||||||
|
"components": {
|
||||||
|
"battery": true,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"grid": true,
|
||||||
|
"load_meter": true,
|
||||||
|
"solar": true,
|
||||||
|
"solar_type": "pv_panel",
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"device_id": "",
|
||||||
|
"din": "",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"energy_left": 13136.26315789474,
|
||||||
|
"energy_site_id": 247079748146,
|
||||||
|
"features": {
|
||||||
|
"rate_plan_manager_no_pricing_constraint": true
|
||||||
|
},
|
||||||
|
"gateway_id": "a",
|
||||||
|
"go_off_grid_test_banner_enabled": null,
|
||||||
|
"id": "1234",
|
||||||
|
"percentage_charged": 31.52375310862408,
|
||||||
|
"powerwall_onboarding_settings_set": true,
|
||||||
|
"powerwall_tesla_electric_interested_in": null,
|
||||||
|
"resource_type": "battery",
|
||||||
|
"site_name": "",
|
||||||
|
"storm_mode_enabled": true,
|
||||||
|
"sync_grid_alert_enabled": true,
|
||||||
|
"total_pack_energy": 41671,
|
||||||
|
"vpp_tour_enabled": null,
|
||||||
|
"warp_site_number": "STE00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
70
testdata/products_gak_2024_04_12.json
vendored
Normal file
70
testdata/products_gak_2024_04_12.json
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"access_type": "OWNER",
|
||||||
|
"api_version": 73,
|
||||||
|
"backseat_token": null,
|
||||||
|
"backseat_token_updated_at": null,
|
||||||
|
"ble_autopair_enrolled": false,
|
||||||
|
"cached_data": "",
|
||||||
|
"calendar_enabled": true,
|
||||||
|
"color": null,
|
||||||
|
"command_signing": "required",
|
||||||
|
"display_name": "Kool Beans",
|
||||||
|
"granular_access": {
|
||||||
|
"hide_private": false
|
||||||
|
},
|
||||||
|
"id": 123,
|
||||||
|
"id_s": "123",
|
||||||
|
"in_service": false,
|
||||||
|
"option_codes": null,
|
||||||
|
"release_notes_supported": true,
|
||||||
|
"state": "online",
|
||||||
|
"tokens": [],
|
||||||
|
"user_id": 123,
|
||||||
|
"vehicle_id": 123,
|
||||||
|
"vin": "LRW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset_site_id": "qwfp",
|
||||||
|
"backup_capable": true,
|
||||||
|
"battery_power": -1590,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"breaker_alert_enabled": true,
|
||||||
|
"components": {
|
||||||
|
"battery": true,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"grid": true,
|
||||||
|
"load_meter": true,
|
||||||
|
"market_type": "residential",
|
||||||
|
"solar": true,
|
||||||
|
"solar_type": "pv_panel",
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"device_id": "qwfp",
|
||||||
|
"din": "qwfp",
|
||||||
|
"is_active": true,
|
||||||
|
"part_number": "1529455-02-D"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"energy_site_id": 1234,
|
||||||
|
"features": {
|
||||||
|
"rate_plan_manager_no_pricing_constraint": true
|
||||||
|
},
|
||||||
|
"gateway_id": "qwfp",
|
||||||
|
"go_off_grid_test_banner_enabled": null,
|
||||||
|
"id": "qwfp",
|
||||||
|
"percentage_charged": 48.872760297620715,
|
||||||
|
"powerwall_onboarding_settings_set": true,
|
||||||
|
"powerwall_tesla_electric_interested_in": null,
|
||||||
|
"resource_type": "battery",
|
||||||
|
"site_name": "",
|
||||||
|
"storm_mode_enabled": true,
|
||||||
|
"sync_grid_alert_enabled": true,
|
||||||
|
"vpp_tour_enabled": null,
|
||||||
|
"warp_site_number": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue