prometheus + climate state!

This commit is contained in:
Alex Janka 2024-01-09 11:11:16 +11:00
parent 3706d7a527
commit d58633ca54
5 changed files with 241 additions and 14 deletions

141
Cargo.lock generated
View file

@ -17,6 +17,18 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.2" version = "1.1.2"
@ -95,6 +107,12 @@ version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9"
[[package]]
name = "arc-swap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "2.1.1" version = "2.1.1"
@ -406,6 +424,17 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam-epoch"
version = "0.9.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.18" version = "0.8.18"
@ -722,6 +751,15 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "hashbrown"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.3" version = "0.14.3"
@ -905,7 +943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.14.3",
"serde", "serde",
] ]
@ -1023,6 +1061,45 @@ version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "metrics"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77b9e10a211c839210fd7f99954bda26e5f8e26ec686ad68da6a32df7c80e782"
dependencies = [
"ahash",
"portable-atomic",
]
[[package]]
name = "metrics-prometheus"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e1316f9ef05b91f4d0e0a0da5b620ba919d336280b83b36be096b86c030fdd"
dependencies = [
"arc-swap",
"metrics",
"metrics-util",
"once_cell",
"prometheus",
"sealed",
"smallvec",
"thiserror",
]
[[package]]
name = "metrics-util"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2670b8badcc285d486261e2e9f1615b506baff91427b61bd336a472b65bbf5ed"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"hashbrown 0.13.1",
"metrics",
"num_cpus",
]
[[package]] [[package]]
name = "miette" name = "miette"
version = "5.10.0" version = "5.10.0"
@ -1306,6 +1383,12 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
[[package]]
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -1340,6 +1423,27 @@ dependencies = [
"yansi 1.0.0-rc.1", "yansi 1.0.0-rc.1",
] ]
[[package]]
name = "prometheus"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c"
dependencies = [
"cfg-if",
"fnv",
"lazy_static",
"memchr",
"parking_lot",
"protobuf",
"thiserror",
]
[[package]]
name = "protobuf"
version = "2.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
[[package]] [[package]]
name = "psl-types" name = "psl-types"
version = "2.0.11" version = "2.0.11"
@ -1754,6 +1858,18 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "sealed"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.43",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.9.2" version = "2.9.2"
@ -2056,6 +2172,9 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"include_dir", "include_dir",
"metrics",
"metrics-prometheus",
"prometheus",
"rocket", "rocket",
"ron", "ron",
"serde", "serde",
@ -2769,6 +2888,26 @@ dependencies = [
"is-terminal", "is-terminal",
] ]
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.7.0" version = "1.7.0"

View file

@ -24,3 +24,6 @@ anyhow = "1.0"
include_dir = "0.7" include_dir = "0.7"
chrono = "0.4" chrono = "0.4"
async-channel = "2.1" async-channel = "2.1"
metrics = "0.22"
metrics-prometheus = "0.6.0"
prometheus = "0.13"

View file

@ -1,4 +1,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use metrics::{describe_gauge, gauge, Gauge, Unit};
use metrics_prometheus::Recorder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
path::PathBuf, path::PathBuf,
@ -13,12 +15,41 @@ use teslatte::{
use crate::{errors::*, types::CarState}; 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 struct TeslaInterface {
pub state: Arc<RwLock<CarState>>, pub state: Arc<RwLock<CarState>>,
api: FleetApi, api: FleetApi,
vehicle: Box<teslatte::vehicles::VehicleData>, vehicle: Box<teslatte::vehicles::VehicleData>,
last_refresh: Instant, last_refresh: Instant,
auth_path: PathBuf, auth_path: PathBuf,
metrics: Metrics,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@ -52,12 +83,15 @@ impl TeslaInterface {
.next() .next()
.context("No vehicles attached to account!")?; .context("No vehicles attached to account!")?;
let (metrics, _recorder) = Metrics::new();
let interface = Self { let interface = Self {
state: Arc::new(RwLock::new(Default::default())), state: Arc::new(RwLock::new(Default::default())),
api, api,
last_refresh, last_refresh,
auth_path, auth_path,
vehicle, vehicle,
metrics,
}; };
interface.save_key()?; interface.save_key()?;
@ -91,23 +125,30 @@ impl TeslaInterface {
} }
async fn refresh_state(&mut self) { async fn refresh_state(&mut self) {
match get_state(&self.api, self.vehicle.id.clone()).await { match get_state(&self.api, self.vehicle.id).await {
Ok(new_state) => { Ok(new_state) => {
self.last_refresh = Instant::now(); self.last_refresh = Instant::now();
let mut state = self.state.write().expect("State handler panicked!!"); let mut state = self.state.write().expect("State handler panicked!!");
if let Some(new_charge_state) = new_state.charge_state { 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); state.charge_state = Some(new_charge_state);
} }
if let Some(new_location_data) = new_state.location_data { if let Some(new_location_data) = new_state.location_data {
state.location_data = Some(new_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) => eprintln!("Error getting charge state: {e:#?}"), }
Err(e) => eprintln!("Error getting state: {e:#?}"),
} }
} }
#[allow(unused)]
async fn refresh_keys(&mut self) { async fn refresh_keys(&mut self) {
if Instant::now().duration_since(self.last_refresh) >= REFRESH_INTERVAL { if Instant::now().duration_since(self.last_refresh) >= REFRESH_INTERVAL {
match self.api.refresh().await { match self.api.refresh().await {
@ -125,25 +166,36 @@ impl TeslaInterface {
} }
async fn get_state(api: &FleetApi, vehicle_id: VehicleId) -> Result<CarState> { async fn get_state(api: &FleetApi, vehicle_id: VehicleId) -> Result<CarState> {
let vehicle_data = api let charge_state = api
.vehicle_data(&GetVehicleData { .vehicle_data(&GetVehicleData {
vehicle_id: vehicle_id.clone(), vehicle_id,
endpoints: vec![Endpoint::ChargeState].into(), endpoints: vec![Endpoint::ChargeState].into(),
// endpoints: vec![Endpoint::VehicleDataCombo].into(),
}) })
.await?; .await?
let charge_state = vehicle_data.charge_state.map(|v| v.into()); .charge_state
.map(|v| v.into());
let vehicle_data = api let location_data = api
.vehicle_data(&GetVehicleData { .vehicle_data(&GetVehicleData {
vehicle_id, vehicle_id,
endpoints: vec![Endpoint::LocationData].into(), endpoints: vec![Endpoint::LocationData].into(),
}) })
.await?; .await?
let location_data = vehicle_data.drive_state.and_then(|v| v.try_into().ok()); .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 { Ok(CarState {
charge_state, charge_state,
location_data, location_data,
climate_state,
}) })
} }

View file

@ -11,7 +11,7 @@ use rocket::{
use crate::{ use crate::{
api_interface::InterfaceRequest, api_interface::InterfaceRequest,
config::Config, config::Config,
types::{CarState, ChargeState}, types::{CarState, ChargeState, ClimateState},
}; };
use self::static_handler::UiStatic; use self::static_handler::UiStatic;
@ -42,7 +42,10 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
.attach(Cors) .attach(Cors)
.manage(state) .manage(state)
.mount("/", fileserver) .mount("/", fileserver)
.mount("/", routes![home, charge_state, flash]) .mount(
"/",
routes![home, charge_state, flash, climate_state, metrics],
)
} }
#[get("/home")] #[get("/home")]
@ -56,11 +59,23 @@ async fn charge_state(state: &State<ServerState>) -> Option<Json<ChargeState>> {
Some(Json(state.state.read().ok()?.charge_state?)) Some(Json(state.state.read().ok()?.charge_state?))
} }
#[get("/climate-state")]
async fn climate_state(state: &State<ServerState>) -> Option<Json<ClimateState>> {
Some(Json(state.state.read().ok()?.climate_state?))
}
#[post("/flash")] #[post("/flash")]
async fn flash(state: &State<ServerState>) { async fn flash(state: &State<ServerState>) {
let _ = state.api_requests.send(InterfaceRequest::FlashLights).await; let _ = state.api_requests.send(InterfaceRequest::FlashLights).await;
} }
#[get("/metrics")]
fn metrics() -> Option<String> {
prometheus::TextEncoder::new()
.encode_to_string(&prometheus::default_registry().gather())
.ok()
}
pub struct Cors; pub struct Cors;
#[rocket::async_trait] #[rocket::async_trait]

View file

@ -6,6 +6,24 @@ use serde::{Deserialize, Serialize};
pub struct CarState { pub struct CarState {
pub charge_state: Option<ChargeState>, pub charge_state: Option<ChargeState>,
pub location_data: Option<LocationData>, pub location_data: Option<LocationData>,
pub climate_state: Option<ClimateState>,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct ClimateState {
pub inside_temp: f64,
}
impl TryFrom<teslatte::vehicles::ClimateState> for ClimateState {
type Error = anyhow::Error;
fn try_from(value: teslatte::vehicles::ClimateState) -> Result<Self, Self::Error> {
Ok(Self {
inside_temp: value
.inside_temp
.context("no temperature in climate data")?,
})
}
} }
#[derive(Clone, Copy, Serialize, Deserialize, Debug)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]