Compare commits

...

32 commits

Author SHA1 Message Date
Alex Janka ef26ce4697 charge state: complete 2024-05-24 19:42:48 +10:00
Alex Janka fd1794744b cop&hvac enums 2024-05-24 19:42:48 +10:00
Alex Janka 3add4893f9 "parked" is a stupid name 2024-05-24 19:42:48 +10:00
Alex Janka b13c40429f shift state enum 2024-05-24 19:42:48 +10:00
Alex Janka 64aa35a969 charging state as enum 2024-05-24 19:42:48 +10:00
Alex Janka a2bc59fb34 fix speed type 2024-05-24 19:42:48 +10:00
Alex Janka 9ecf21554e derive Copy on VehicleId 2024-05-24 19:42:47 +10:00
Alex Janka 677d959e6d get rid of this one warning 2024-05-24 19:42:47 +10:00
Alex Janka 78d347f38e reverse proxied api url 2024-05-24 19:42:47 +10:00
Alex Janka 3123cad1a5 my changes 2024-05-24 19:42:44 +10:00
gak 13f4be3478
chore: v0.1.15 2024-04-12 11:14:32 +10:00
gak e326d17c14
chore: changelog 2024-04-12 11:14:20 +10:00
gak 6f7fb72943
fix: another invalid total_pack_energy 2024-04-12 11:10:15 +10:00
gak 62b3343b21
chore: v0.1.14 2024-04-12 08:53:29 +10:00
gak 19ac4b485d
chore: changelog release 2024-04-12 08:52:41 +10:00
gak 1e6c1d5ff3
fix: energy_left total_pack_energy` gone. Untagged handling 2024-04-12 08:49:20 +10:00
gak fcc9e63930
Merge pull request #18 from gak/dependabot/cargo/miette-7.2.0
chore(deps): update miette requirement from 5.10.0 to 7.2.0
2024-04-12 08:49:06 +10:00
gak afd833f13f
Merge pull request #23 from gak/dependabot/cargo/reqwest-0.12.3
chore(deps): update reqwest requirement from 0.11.23 to 0.12.3
2024-04-12 08:48:49 +10:00
gak 6723c0ad02
Merge pull request #24 from gak/dependabot/cargo/colored_json-5.0.0
chore(deps): update colored_json requirement from 4.1.0 to 5.0.0
2024-04-12 08:48:40 +10:00
dependabot[bot] 8b5141376e
chore(deps): update colored_json requirement from 4.1.0 to 5.0.0
Updates the requirements on [colored_json](https://github.com/ctron/colored_json) to permit the latest version.
- [Release notes](https://github.com/ctron/colored_json/releases)
- [Commits](https://github.com/ctron/colored_json/compare/v4.1.0...v5.0.0)

---
updated-dependencies:
- dependency-name: colored_json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 08:42:32 +00:00
dependabot[bot] 0de5ee26b9
chore(deps): update reqwest requirement from 0.11.23 to 0.12.3
Updates the requirements on [reqwest](https://github.com/seanmonstar/reqwest) to permit the latest version.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.23...v0.12.3)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 08:42:13 +00:00
dependabot[bot] 906ce9cf04
chore(deps): update miette requirement from 5.10.0 to 7.2.0
Updates the requirements on [miette](https://github.com/zkat/miette) to permit the latest version.
- [Release notes](https://github.com/zkat/miette/releases)
- [Changelog](https://github.com/zkat/miette/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkat/miette/compare/miette-derive-v5.10.0...miette-derive-v7.2.0)

---
updated-dependencies:
- dependency-name: miette
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-08 08:55:59 +00:00
gak b7e2f94884
Merge pull request #17 from gak/dependabot/cargo/rustls-0.23.0
chore(deps): update rustls requirement from 0.22.2 to 0.23.0
2024-03-01 21:52:58 +11:00
dependabot[bot] 63015deb1b
chore(deps): update rustls requirement from 0.22.2 to 0.23.0
Updates the requirements on [rustls](https://github.com/rustls/rustls) to permit the latest version.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.22.2...v/0.23.0)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 08:53:16 +00:00
gak 325b899504
Merge pull request #14 from gak/dependabot/cargo/strum-0.26.1
chore(deps): update strum requirement from 0.25.0 to 0.26.1
2024-01-29 21:47:37 +11:00
dependabot[bot] a16ca64151
chore(deps): update strum requirement from 0.25.0 to 0.26.1
---
updated-dependencies:
- dependency-name: strum
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 08:16:42 +00:00
gak 4ba1dd2c33
chore: v0.1.13 2024-01-24 10:26:40 +11:00
gak 796c09c17a
fix: remove vehicles list (#13) 2024-01-24 10:25:41 +11:00
gak c8b45d5c3e
chore: v0.1.12 2024-01-20 19:46:12 +11:00
gak 280f5751cc
chore: more deps 2024-01-20 19:42:17 +11:00
gak c2b12df27b
chore: dep upgrades 2024-01-20 19:42:13 +11:00
gak 85e3728a1d
fix: powerwall api new fields 2024-01-20 19:40:35 +11:00
14 changed files with 707 additions and 127 deletions

View file

@ -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/),
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
### Changed
- `vehicle_data` now accepts `GetVehicleData` instead of `VehicleId`. (See [6facc27](https://github.com/gak/teslatte/commit/6facc27d8b408d35b98b4c6c0ad3e5df82328d2c))
## [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
- API changes for "api_version 67"
- VehicleData new fields:
- cached_data
- command_signing
- release_notes_supported
- ClimateState new fields:
- auto_steering_wheel_heat
- cop_activation_temperature,
- steering_wheel_heat_level
- DriveState now Optional:
- gps_as_of
- heading
- latitude
@ -37,16 +77,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- native_type
- DriveState new fields:
- active_route_traffic_minutes_delay
- GuiSettings new field:
- gui_tirepressure_units
- VehicleConfig new fields:
- cop_user_set_temp_supported
- webcam_selfie_supported
- VehicleState new fields:
- media_info: MediaInfo
- tpms_hard_warning_fl
- tpms_hard_warning_fr

View file

@ -1,6 +1,6 @@
[package]
name = "teslatte"
version = "0.1.11"
version = "0.1.15"
description = "A command line tool and Rust crate for querying the Tesla API."
edition = "2021"
rust-version = "1.75.0"
@ -20,25 +20,25 @@ path = "src/main.rs"
required-features = ["cli"]
[dependencies]
miette = { version = "5.10.0", features = ["fancy"] }
thiserror = "1.0.50"
tokio = { version = "1.33.0", features = ["full"] }
miette = { version = "7.2.0", features = ["fancy"] }
thiserror = "1.0.56"
tokio = { version = "1.35.1", features = ["full"] }
tracing = "0.1.40"
reqwest = { version = "0.11.22", features = ["rustls-tls", "cookies", "json"] }
url = "2.4.1"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
rustls = "0.22.1"
reqwest = { version = "0.12.3", features = ["rustls-tls", "cookies", "json"] }
url = "2.5.0"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
rustls = "0.23.0"
rand = "0.8.5"
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"
derive_more = "0.99.17"
pkce = "0.2.0"
clap = { version = "4.4.6", features = ["derive", "env"], optional = true }
tracing-subscriber = { version = "0.3.17", optional = true }
colored_json = { version = "4.0.0", optional = true }
clap = { version = "4.4.18", features = ["derive", "env"], optional = true }
tracing-subscriber = { version = "0.3.18", optional = true }
colored_json = { version = "5.0.0", optional = true }
[dev-dependencies]
test-log = { version = "0.2.13", default-features = false, features = ["trace"] }
test-log = { version = "0.2.14", default-features = false, features = ["trace"] }

View file

@ -1,8 +1,7 @@
use std::env;
use teslatte::auth::AccessToken;
use teslatte::products::Product;
use teslatte::vehicles::GetVehicleData;
use teslatte::{OwnerApi, VehicleApi};
use teslatte::OwnerApi;
#[tokio::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();
dbg!(&*products);
dbg!(&products);
if !products.is_empty() {
for product in &*products {

View file

@ -7,9 +7,8 @@ no_token_test:
# Require an access token from "cli.json". Use `just auth` to generate.
token_tests:
cargo run -- api vehicles
cargo run --no-default-features --features cli -- api vehicles
cargo run -- api products
cargo run --no-default-features --features cli -- api products
publish version:
git diff-index --quiet HEAD
@ -30,3 +29,7 @@ auth:
audit:
cargo audit
update:
cargo update && cargo upgrade
cd tesla_api_coverage && cargo update && cargo upgrade

View file

@ -1,5 +1,5 @@
use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState};
use crate::{OwnerApi, TeslatteError};
use crate::{FleetApi, OwnerApi, TeslatteError};
use derive_more::{Display, FromStr};
use rand::Rng;
use reqwest::Client;
@ -10,6 +10,7 @@ use url::Url;
const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize";
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)]
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)]
struct RefreshTokenRequest {
grant_type: String,

View file

@ -18,7 +18,6 @@ pub struct SiteStatus {
pub battery_power: i64,
pub battery_type: String,
pub breaker_alert_enabled: bool,
pub energy_left: f64,
pub gateway_id: String,
pub percentage_charged: f64,
pub powerwall_onboarding_settings_set: bool,
@ -29,14 +28,12 @@ pub struct SiteStatus {
pub site_name: String,
pub storm_mode_enabled: bool,
pub sync_grid_alert_enabled: bool,
pub total_pack_energy: i64,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct LiveStatus {
pub backup_capable: bool,
pub battery_power: i64,
pub energy_left: f64,
pub generator_power: i64,
pub grid_power: i64,
pub grid_services_active: bool,
@ -48,7 +45,6 @@ pub struct LiveStatus {
pub solar_power: i64,
pub storm_mode_active: bool,
pub timestamp: String,
pub total_pack_energy: i64,
pub wall_connectors: Vec<LiveWallConnector>,
}
@ -69,7 +65,7 @@ pub struct LiveWallConnector {
/// 2: not plugged in
/// 4: plugged in (not charging)
pub wall_connector_state: i64,
pub wall_connector_power: i64,
pub wall_connector_power: f32,
pub wall_connector_fault_state: i64,
}
@ -311,7 +307,7 @@ mod tests {
vin: Some("1234".to_string()),
din: "5432".to_string(),
wall_connector_state: 4,
wall_connector_power: 0,
wall_connector_power: 0.,
wall_connector_fault_state: 2,
}
);
@ -336,7 +332,7 @@ mod tests {
vin: None,
din: "1234".to_string(),
wall_connector_state: 2,
wall_connector_power: 0,
wall_connector_power: 0.,
wall_connector_fault_state: 2,
}
);

View file

@ -25,10 +25,13 @@ pub mod vehicles;
#[cfg(feature = "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 {
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
async fn vehicle_data(
&self,
get_vehicle_data: &GetVehicleData,
@ -98,7 +101,60 @@ pub trait VehicleApi {
) -> 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 {
fn format(&self, url: &str) -> String;
@ -107,7 +163,7 @@ trait ApiValues {
/// Vehicle ID used by the owner-api endpoint.
///
/// 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);
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)]
struct ResponseDeserializer<T> {
error: Option<ResponseError>,
@ -314,6 +499,7 @@ struct ResponseError {
struct Empty {}
/// GET /api/1/[url]
#[allow(unused_macros)]
macro_rules! get {
($name:ident, $return_type:ty, $url:expr) => {
async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> {
@ -324,6 +510,7 @@ macro_rules! get {
}
};
}
#[allow(unused_imports)]
pub(crate) use get;
/// Same as get, but public.

View file

@ -4,7 +4,7 @@ use teslatte::auth::{AccessToken, RefreshToken};
use teslatte::cli::energy::EnergySiteArgs;
use teslatte::cli::powerwall::PowerwallArgs;
use teslatte::cli::vehicle::VehicleArgs;
use teslatte::{OwnerApi, PrintResponses, VehicleApi};
use teslatte::{OwnerApi, PrintResponses};
/// Teslatte
///
@ -106,7 +106,7 @@ async fn main() -> miette::Result<()> {
api.print_responses = PrintResponses::Pretty;
match api_args.command {
ApiCommand::Vehicles => {
api.vehicles().await?;
panic!("Tesla API has changed. This command is no longer works with the Owners API.");
}
ApiCommand::Vehicle(v) => {
v.run(&api).await?;

View file

@ -2,9 +2,10 @@ use crate::energy_sites::WallConnector;
use crate::error::TeslatteError;
use crate::powerwall::PowerwallId;
use crate::vehicles::VehicleData;
use crate::{pub_get, OwnerApi};
use crate::{pub_get, FleetApi, OwnerApi};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::str::FromStr;
#[rustfmt::skip]
@ -12,6 +13,11 @@ impl OwnerApi {
pub_get!(products, Vec<Product>, "/products");
}
#[rustfmt::skip]
impl FleetApi {
pub_get!(products, Vec<Product>, "/products");
}
#[derive(Debug, Clone, Deserialize, Display)]
pub struct EnergySiteId(pub u64);
@ -28,14 +34,44 @@ impl FromStr for EnergySiteId {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayId(String);
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[derive(Debug, Clone)]
pub enum Product {
Vehicle(Box<VehicleData>),
Solar(Box<SolarData>),
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
#[derive(Debug, Clone, Deserialize)]
pub struct SolarData {
@ -61,14 +97,25 @@ pub struct PowerwallData {
pub id: PowerwallId,
pub gateway_id: GatewayId,
pub asset_site_id: String,
pub energy_left: f64,
pub total_pack_energy: i64,
pub percentage_charged: f64,
pub backup_capable: bool,
pub battery_power: i64,
pub sync_grid_alert_enabled: bool,
pub breaker_alert_enabled: bool,
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)]
@ -79,7 +126,7 @@ pub struct Components {
pub solar_type: Option<String>,
pub grid: bool,
pub load_meter: bool,
pub market_type: String,
pub market_type: Option<String>,
#[serde(default)]
pub wall_connectors: Vec<WallConnector>,
}
@ -88,60 +135,9 @@ pub struct Components {
mod tests {
use super::*;
use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
use crate::ApiValues;
use crate::{ApiValues, PrintResponses, RequestData};
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]
fn energy_match_vehicle() {
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"
);
}
#[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
View 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-----

View file

@ -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
// will cause the deserializer to fail. Please log an issue to fix these if you come across it.
use crate::{
get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi,
VehicleApi, VehicleId,
get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, FleetApi,
FleetVehicleApi, OwnerApi, VehicleApi, VehicleId,
};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use strum::{Display, EnumString};
#[rustfmt::skip]
impl VehicleApi for OwnerApi {
get!(vehicles, Vec<Vehicle>, "/vehicles");
get_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData);
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);
}
#[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)]
#[strum(serialize_all = "snake_case")]
pub enum Endpoint {
@ -185,7 +217,7 @@ pub struct ChargeState {
pub charger_pilot_current: Option<i64>,
pub charger_power: Option<i64>,
pub charger_voltage: Option<i64>,
pub charging_state: String,
pub charging_state: ChargingState,
pub conn_charge_cable: String,
pub est_battery_range: f64,
pub fast_charger_brand: String,
@ -208,8 +240,8 @@ pub struct ChargeState {
pub scheduled_charging_start_time: Option<i64>,
pub scheduled_charging_start_time_app: Option<i64>,
pub scheduled_charging_start_time_minutes: Option<i64>,
pub scheduled_departure_time: i64,
pub scheduled_departure_time_minutes: i64,
pub scheduled_departure_time: Option<i64>,
pub scheduled_departure_time_minutes: Option<i64>,
pub supercharger_session_trip_planner: bool,
pub time_to_full_charge: f64,
pub timestamp: u64,
@ -218,6 +250,16 @@ pub struct ChargeState {
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)]
pub struct ClimateState {
pub allow_cabin_overheat_protection: bool,
@ -225,13 +267,13 @@ pub struct ClimateState {
pub auto_seat_climate_right: Option<bool>,
pub battery_heater: 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 climate_keeper_mode: String,
pub defrost_mode: i64,
pub driver_temp_setting: f64,
pub fan_status: i64,
pub hvac_auto_request: String,
pub hvac_auto_request: HvacAutoRequest,
pub inside_temp: Option<f64>,
pub is_auto_conditioning_on: Option<bool>,
pub is_climate_on: bool,
@ -261,6 +303,23 @@ pub struct ClimateState {
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)]
pub struct DriveState {
/// From https://developer.tesla.com/docs/fleet-api#vehicle_data
@ -276,14 +335,42 @@ pub struct DriveState {
pub native_type: Option<String>,
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.
pub speed: Option<String>,
pub speed: Option<i64>,
pub timestamp: i64,
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)]
pub struct GuiSettings {
pub gui_24_hour_time: bool,
@ -458,9 +545,7 @@ pub struct GranularAccess {
pub hide_private: bool,
}
#[derive(Debug, Deserialize)]
pub struct Vehicles(Vec<Vehicle>);
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct Vehicle {
pub id: VehicleId,

View file

@ -4,18 +4,18 @@ version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = "0.11.22"
tokio = { version = "1.33.0", features = ["full"] }
clap = { version = "4.4.6", features = ["derive"] }
scraper = "0.17.1"
reqwest = "0.11.23"
tokio = { version = "1.35.1", features = ["full"] }
clap = { version = "4.4.18", features = ["derive"] }
scraper = "0.18.1"
strum = { version = "0.25.0", features = ["derive"] }
tracing-subscriber = "0.3.17"
tracing-subscriber = "0.3.18"
tracing = "0.1.40"
log = "0.4.20"
nom = "7.1.3"
anyhow = "1.0.75"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
anyhow = "1.0.79"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
heck = "0.5.0-rc.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
View 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
View 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": ""
}
]
}