use metrics::{describe_gauge, gauge, Gauge, Unit}; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, sync::{Arc, RwLock}, time::{Duration, Instant}, }; use teslatte::{ auth::{AccessToken, RefreshToken}, vehicles::{Endpoint, GetVehicleData}, FleetApi, FleetVehicleApi, }; use crate::{errors::*, types::CarState}; struct Metrics { battery_level: Gauge, charge_rate: Gauge, charge_request: Gauge, inside_temp: Gauge, outside_temp: Gauge, battery_heater: Gauge, climate_on: Gauge, preconditioning: Gauge, remote_heater_control_enabled: Gauge, tesla_online: Gauge, } impl Metrics { fn new() -> Self { describe_gauge!("tesla_battery_level", Unit::Percent, "Battery level"); let battery_level = gauge!("tesla_battery_level"); describe_gauge!("tesla_charge_rate", "Charge rate"); let charge_rate = gauge!("tesla_charge_rate"); describe_gauge!("tesla_charge_request", "Requested charge rate"); let charge_request = gauge!("tesla_charge_request"); describe_gauge!("tesla_inside_temp", "Inside temperature"); let inside_temp = gauge!("tesla_inside_temp"); describe_gauge!("tesla_outside_temp", "Outside temperature"); let outside_temp = gauge!("tesla_outside_temp"); describe_gauge!("tesla_battery_heater", "Battery heater"); let battery_heater = gauge!("tesla_battery_heater"); describe_gauge!("tesla_climate_on", "Climate control"); let climate_on = gauge!("tesla_climate_on"); describe_gauge!("tesla_preconditioning", "Preconditioning"); let preconditioning = gauge!("tesla_preconditioning"); describe_gauge!( "tesla_remote_heater_control_enabled", "Remote heater control enabled" ); let remote_heater_control_enabled = gauge!("tesla_remote_heater_control_enabled"); describe_gauge!("tesla_online", "Tesla online"); let tesla_online = gauge!("tesla_online"); Self { battery_level, charge_rate, charge_request, inside_temp, outside_temp, battery_heater, climate_on, preconditioning, remote_heater_control_enabled, tesla_online, } } } pub struct TeslaInterface { pub state: Arc>, api: FleetApi, vehicle: Box, last_refresh: Instant, auth_path: PathBuf, metrics: Metrics, last_cop_state: String, last_climate_keeper: String, last_hvac_auto: String, } #[derive(Serialize, Deserialize, Clone)] struct AuthInfo { access_token: AccessToken, refresh_token: Option, } #[derive(Clone, Copy, Debug)] // these are the messages that the webserver can send the api thread pub enum InterfaceRequest { FlashLights, } const KEY_REFRESH_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60); impl TeslaInterface { pub async fn load(auth_path: PathBuf) -> Result { let key: AuthInfo = ron::from_str(&std::fs::read_to_string(&auth_path)?)?; let mut api = FleetApi::new(key.access_token, key.refresh_token); api.refresh().await?; let last_refresh = Instant::now(); info!("Refreshed auth key"); let vehicle = api .products() .await? .into_iter() .filter_map(|v| match v { teslatte::products::Product::Vehicle(vehicle) => Some(vehicle), _ => None, }) .next() .ok_or(AuthLoadError::NoVehicles)?; let metrics = Metrics::new(); let interface = Self { state: Arc::new(RwLock::new(Default::default())), api, last_refresh, auth_path, vehicle, metrics, last_cop_state: String::new(), last_climate_keeper: String::new(), last_hvac_auto: String::new(), }; interface.save_key()?; Ok(interface) } fn save_key(&self) -> Result<(), SaveError> { std::fs::write( self.auth_path.clone(), ron::ser::to_string(&AuthInfo { access_token: self.api.access_token.clone(), refresh_token: self.api.refresh_token.clone(), })?, )?; info!("Auth successfully saved"); Ok(()) } pub async fn refresh(&mut self) { info!("refreshing..."); self.refresh_keys().await; self.refresh_state().await; } pub async fn process_request(&mut self, request: InterfaceRequest) { match request { InterfaceRequest::FlashLights => { let _ = self.api.flash_lights(&self.vehicle.vin).await; } } } async fn refresh_state(&mut self) { match self.get_state().await { Ok(new_state) => { self.metrics.tesla_online.set(1.); let mut state = self .state .write() .expect("Tesla API state handler panicked!!"); if let Some(new_charge_state) = new_state.charge_state { self.metrics .battery_level .set(new_charge_state.battery_level as f64); self.metrics.charge_rate.set(new_charge_state.charge_rate); self.metrics .charge_request .set(new_charge_state.charge_current_request as f64); state.charge_state = Some(new_charge_state); } if let Some(new_location_data) = new_state.location_data { state.location_data = Some(new_location_data); } if let Some(new_climate_state) = new_state.climate_state { self.metrics.inside_temp.set(new_climate_state.inside_temp); self.metrics .outside_temp .set(new_climate_state.outside_temp); self.metrics .battery_heater .set(bf(new_climate_state.battery_heater)); self.metrics .climate_on .set(bf(new_climate_state.climate_on)); self.metrics .preconditioning .set(bf(new_climate_state.preconditioning)); self.metrics .remote_heater_control_enabled .set(bf(new_climate_state.remote_heater_control_enabled)); state.climate_state = Some(new_climate_state); } } Err(e) => { self.metrics.tesla_online.set(0.); if let RequestError::Teslatte(teslatte::error::TeslatteError::DecodeJsonError { source: _, request: _, body, }) = &e { if body.contains("vehicle is offline or asleep") { return; } } match e { RequestError::StdIo(e) => log::error!("Stdio error: {e:?}"), RequestError::RonSpanned(e) => log::error!("RON parsing error: {e:?}"), RequestError::Teslatte(teslatte::error::TeslatteError::DecodeJsonError { source, request: _, body, }) => { log::error!("Error {source} getting state: {body}") } RequestError::Teslatte(e) => log::error!("Teslatte error: {e:?}"), RequestError::Save(e) => log::error!("Save error: {e:?}"), } } } } async fn refresh_keys(&mut self) { // refresh our Tesla (the company's web servers, not the car) access token if Instant::now().duration_since(self.last_refresh) >= KEY_REFRESH_INTERVAL { log::warn!("refreshing keys..."); if self .api .refresh() .await .some_or_print_with("refreshing key") .is_some() { let now = Instant::now(); if self.save_key().some_or_print_with("saving key").is_some() { self.last_refresh = now; } } } } async fn get_state(&mut self) -> Result { // Endpoint::VehicleDataCombo or multiple Endpoints in one request // doesn't seem to reliably get them all, // so for each endpoint we do a new request let charge_state = self .api .vehicle_data(&GetVehicleData { vehicle_id: self.vehicle.id, endpoints: vec![Endpoint::ChargeState].into(), }) .await? .charge_state .map(|v| v.into()); let location_data = self .api .vehicle_data(&GetVehicleData { vehicle_id: self.vehicle.id, endpoints: vec![Endpoint::LocationData].into(), }) .await? .drive_state .and_then(|v| v.try_into().ok()); let climate_state = self .api .vehicle_data(&GetVehicleData { vehicle_id: self.vehicle.id, endpoints: vec![Endpoint::ClimateState].into(), }) .await? .climate_state .and_then(|v| { if self.last_cop_state != v.cabin_overheat_protection { log::warn!( "Current cabin overheat protection state: {}", v.cabin_overheat_protection ); self.last_cop_state = v.cabin_overheat_protection.clone(); } if self.last_climate_keeper != v.climate_keeper_mode { log::warn!("Current climate keeper mode: {}", v.climate_keeper_mode); self.last_climate_keeper = v.climate_keeper_mode.clone(); } if self.last_hvac_auto != v.hvac_auto_request { log::warn!("HVAC auto request set to: {}", v.hvac_auto_request); self.last_hvac_auto = v.hvac_auto_request.clone(); } v.try_into().ok() }); Ok(CarState { charge_state, location_data, climate_state, }) } } fn bf(value: bool) -> f64 { if value { 1.0 } else { 0.0 } }