use anyhow::{Context, Result}; use metrics::{describe_gauge, gauge, Gauge, Unit}; use metrics_prometheus::Recorder; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, sync::{Arc, RwLock}, time::{Duration, Instant}, }; use teslatte::{ auth::{AccessToken, RefreshToken}, vehicles::{Endpoint, GetVehicleData}, FleetApi, FleetVehicleApi, VehicleId, }; use crate::{errors::*, types::CarState}; struct Metrics { battery_level: Gauge, charge_rate: Gauge, inside_temp: Gauge, } impl Metrics { fn new() -> (Self, Recorder) { let recorder = metrics_prometheus::install(); 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_inside_temp", "Inside temperature"); let inside_temp = gauge!("tesla_inside_temp"); ( Self { battery_level, charge_rate, inside_temp, }, recorder, ) } } pub struct TeslaInterface { pub state: Arc>, api: FleetApi, vehicle: Box, last_refresh: Instant, auth_path: PathBuf, metrics: Metrics, } #[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(12 * 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() .context("No vehicles attached to account!")?; let (metrics, _recorder) = Metrics::new(); let interface = Self { state: Arc::new(RwLock::new(Default::default())), api, last_refresh, auth_path, vehicle, metrics, }; 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 keys..."); 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 get_state(&self.api, self.vehicle.id).await { Ok(new_state) => { self.last_refresh = Instant::now(); let mut state = self.state.write().expect("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); 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); state.climate_state = Some(new_climate_state); } } Err(e) => error!("Error getting state: {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 { match self.api.refresh().await { Ok(_) => { let now = Instant::now(); match self.save_key() { Ok(_) => self.last_refresh = now, Err(e) => error!("error saving auth token: {e:?}"), } } Err(e) => error!("error refreshing auth token: {e:?}"), } } } } async fn get_state(api: &FleetApi, vehicle_id: VehicleId) -> 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 = api .vehicle_data(&GetVehicleData { vehicle_id, endpoints: vec![Endpoint::ChargeState].into(), }) .await? .charge_state .map(|v| v.into()); let location_data = api .vehicle_data(&GetVehicleData { vehicle_id, endpoints: vec![Endpoint::LocationData].into(), }) .await? .drive_state .and_then(|v| v.try_into().ok()); let climate_state = api .vehicle_data(&GetVehicleData { vehicle_id, endpoints: vec![Endpoint::ClimateState].into(), }) .await? .climate_state .and_then(|v| v.try_into().ok()); Ok(CarState { charge_state, location_data, climate_state, }) }