use anyhow::{Context, Result}; 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, VehicleId, }; 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, 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_online", "Tesla online"); let tesla_online = gauge!("tesla_online"); Self { battery_level, charge_rate, charge_request, inside_temp, outside_temp, battery_heater, tesla_online, } } } pub struct TeslaInterface { pub state: Arc>, api: FleetApi, vehicle: Box, last_refresh: Instant, auth_path: PathBuf, metrics: Metrics, last_cop_state: 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(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 = Metrics::new(); let interface = Self { state: Arc::new(RwLock::new(Default::default())), api, last_refresh, auth_path, vehicle, metrics, last_cop_state: 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 get_state(&self.api, self.vehicle.id, &mut self.last_cop_state).await { Ok(new_state) => { self.metrics.tesla_online.set(1.); 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); 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(if new_climate_state.battery_heater { 1.0 } else { 0.0 }); state.climate_state = Some(new_climate_state); } } Err(e) => { self.metrics.tesla_online.set(0.); 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, last_cop_state: &mut String, ) -> 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| { if *last_cop_state != v.cabin_overheat_protection { log::warn!( "Current cabin overheat protection state: {}", v.cabin_overheat_protection ); *last_cop_state = v.cabin_overheat_protection.clone(); } v.try_into().ok() }); Ok(CarState { charge_state, location_data, climate_state, }) }