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/),
|
||||
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
|
||||
|
|
28
Cargo.toml
28
Cargo.toml
|
@ -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"] }
|
||||
|
|
|
@ -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 {
|
||||
|
|
7
justfile
7
justfile
|
@ -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
|
||||
|
|
65
src/auth.rs
65
src/auth.rs
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
195
src/lib.rs
195
src/lib.rs
|
@ -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.
|
||||
|
|
|
@ -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?;
|
||||
|
|
130
src/products.rs
130
src/products.rs
|
@ -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
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
|
||||
// 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,
|
||||
|
|
|
@ -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
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