Add Prometheus power check
This commit is contained in:
parent
0705966c2e
commit
78ba0a3bac
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -278,10 +278,12 @@ dependencies = [
|
||||
"clap",
|
||||
"easee",
|
||||
"humantime",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tungstenite",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -662,18 +664,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
version = "1.0.205"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
|
||||
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.204"
|
||||
version = "1.0.205"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -13,7 +13,9 @@ anyhow = "1.0.86"
|
||||
clap = { version = "4.5.11", features = ["derive", "env"] }
|
||||
easee = { path = "../easee", features = ["tungstenite"] }
|
||||
humantime = "2.1.0"
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
serde_json = "1.0.121"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tungstenite = { version = "0.23.0", optional = true, features = ["rustls-tls-native-roots"] }
|
||||
ureq = { version = "2.10.0", features = ["json"] }
|
||||
|
221
src/main.rs
221
src/main.rs
@ -1,12 +1,16 @@
|
||||
use std::error::Error;
|
||||
|
||||
use anyhow::{Context as AnyhowContext, Result};
|
||||
use clap::ValueEnum;
|
||||
use clap::{Parser, Subcommand};
|
||||
use anyhow::{Context, Result};
|
||||
use easee::api::ChargingSession;
|
||||
use easee::api::{ApiError, Charger, ChargerState, ChargingSession, Context};
|
||||
use easee::stream;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug,Clone,Copy,ValueEnum)]
|
||||
mod prom;
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum Command {
|
||||
Start,
|
||||
Stop,
|
||||
@ -14,135 +18,172 @@ enum Command {
|
||||
Resume,
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Copy,ValueEnum)]
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum Session {
|
||||
Ongoing,
|
||||
Latest
|
||||
Latest,
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Subcommand)]
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
enum Mode {
|
||||
Login,
|
||||
Status,
|
||||
Session {
|
||||
Session {
|
||||
#[arg(default_value = "ongoing")]
|
||||
session: Session
|
||||
session: Session,
|
||||
},
|
||||
Charge {
|
||||
command: Command,
|
||||
},
|
||||
Charge { command: Command },
|
||||
Stream,
|
||||
Power,
|
||||
}
|
||||
|
||||
#[derive(Debug,Parser)]
|
||||
#[derive(Debug, Parser)]
|
||||
struct CLI {
|
||||
#[arg(short,long)]
|
||||
#[arg(short, long)]
|
||||
debug: bool,
|
||||
|
||||
#[arg(short,long)]
|
||||
#[arg(short, long)]
|
||||
charger_id: Vec<String>,
|
||||
|
||||
#[arg(short, long, default_value = "http://localhost:9090")]
|
||||
prometheus: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
const SAVED_TOKEN_PATH: &str = ".easee_token";
|
||||
|
||||
fn login() -> Result<()> {
|
||||
use std::io::Write;
|
||||
let stdin = std::io::stdin();
|
||||
let mut stderr = std::io::stderr();
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
|
||||
write!(stderr, "Username: ")?;
|
||||
stdin.read_line(&mut username)?;
|
||||
write!(stderr, "Password: ")?;
|
||||
stdin.read_line(&mut password)?;
|
||||
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
|
||||
let ctx = easee::api::Context::from_login(&username, &password)?;
|
||||
eprintln!("Login successful.");
|
||||
|
||||
std::fs::write(SAVED_TOKEN_PATH, ctx.save().as_bytes())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn load_context() -> Result<easee::api::Context> {
|
||||
let saved = std::fs::read_to_string(SAVED_TOKEN_PATH)
|
||||
.context("Cannot read saved token (did you log in ?)")?;
|
||||
Ok(easee::api::Context::from_saved(&saved)?)
|
||||
}
|
||||
|
||||
fn load_chargers(ctx: &mut Context, names: &[String]) -> Result<Vec<Charger>> {
|
||||
let chargers: Vec<Charger>;
|
||||
if names.is_empty() {
|
||||
chargers = ctx.chargers()?;
|
||||
info!("{} chargers available.", chargers.len());
|
||||
} else {
|
||||
chargers = names
|
||||
.iter()
|
||||
.map(|id| ctx.charger(&id))
|
||||
.collect::<Result<_, _>>()?;
|
||||
}
|
||||
Ok(chargers)
|
||||
}
|
||||
|
||||
fn stream(names: &[String]) -> Result<()> {
|
||||
let mut ctx = load_context()?;
|
||||
let mut stream = stream::Stream::open(&mut ctx)?;
|
||||
let chargers = load_chargers(&mut ctx, names)?;
|
||||
for c in &chargers {
|
||||
stream.subscribe(&c.id)?;
|
||||
}
|
||||
|
||||
let mut stream = easee::signalr::Stream::from_ws(stream);
|
||||
loop {
|
||||
println!("{:?}", stream.recv()?);
|
||||
}
|
||||
}
|
||||
|
||||
fn loop_chargers<F, E>(ctx: &mut Context, names: &[String], mut f: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(Charger, &mut Context) -> Result<(), E>,
|
||||
E: Error,
|
||||
{
|
||||
let chargers = load_chargers(ctx, names)?;
|
||||
for c in chargers {
|
||||
if let Err(e) = f(c, ctx) {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn status(id: &str, c: ChargerState) {
|
||||
let cable = if c.cable_locked { "LOCK" } else { "open" };
|
||||
let mode = c.charger_op_mode;
|
||||
let power = c.total_power;
|
||||
println!("{id}: [{mode:?}] ({power}W) cable:{cable}");
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
||||
let args = CLI::parse();
|
||||
if args.debug {
|
||||
tracing::subscriber::set_global_default(tracing_subscriber::FmtSubscriber::new())
|
||||
.expect("Tracing subscriber failed");
|
||||
}
|
||||
|
||||
if let Mode::Login = args.mode {
|
||||
use std::io::Write;
|
||||
let stdin = std::io::stdin();
|
||||
let mut stderr = std::io::stderr();
|
||||
let mut ctx = load_context()?;
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
|
||||
write!(stderr, "Username: ")?;
|
||||
stdin.read_line(&mut username)?;
|
||||
write!(stderr, "Password: ")?;
|
||||
stdin.read_line(&mut password)?;
|
||||
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
|
||||
let ctx = easee::api::Context::from_login(&username, &password)?;
|
||||
eprintln!("Login successful.");
|
||||
|
||||
std::fs::write(SAVED_TOKEN_PATH, ctx.save().as_bytes())?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let saved = std::fs::read_to_string(SAVED_TOKEN_PATH)
|
||||
.context("Cannot read saved token (did you log in ?)")?;
|
||||
let mut ctx = easee::api::Context::from_saved(&saved)?;
|
||||
|
||||
let chargers;
|
||||
if args.charger_id.is_empty() {
|
||||
chargers = ctx.chargers()?;
|
||||
info!("{} chargers available.", chargers.len());
|
||||
} else {
|
||||
chargers = args.charger_id.iter()
|
||||
.map(|id| ctx.charger(&id))
|
||||
.collect::<Result<_,_>>()?;
|
||||
}
|
||||
|
||||
if let Mode::Stream = args.mode {
|
||||
let mut stream = stream::Stream::open(&mut ctx)?;
|
||||
for c in &chargers {
|
||||
stream.subscribe(&c.id)?;
|
||||
match args.mode {
|
||||
Mode::Login => login()?,
|
||||
Mode::Status => loop_chargers(&mut ctx, &args.charger_id, |c, ctx| {
|
||||
c.state(ctx).map(|s| status(&c.id, s))
|
||||
})?,
|
||||
Mode::Session { session } => {
|
||||
let cmd: fn(Charger, &mut Context) -> Result<(), ApiError> = match session {
|
||||
Session::Latest => |c, ctx| c.latest_session(ctx).map(|s| show_session(&s)),
|
||||
Session::Ongoing => |c, ctx| c.ongoing_session(ctx).map(|s| show_session(&s)),
|
||||
};
|
||||
loop_chargers(&mut ctx, &args.charger_id, cmd)?
|
||||
}
|
||||
|
||||
let mut stream = easee::signalr::Stream::from_ws(stream);
|
||||
loop {
|
||||
println!("{:?}", stream.recv()?);
|
||||
Mode::Charge { command } => {
|
||||
let cmd: fn(Charger, &mut Context) -> Result<(), ApiError> = match command {
|
||||
Command::Start => |c, ctx| c.start(ctx),
|
||||
Command::Stop => |c, ctx| c.stop(ctx),
|
||||
Command::Pause => |c, ctx| c.pause(ctx),
|
||||
Command::Resume => |c, ctx| c.resume(ctx),
|
||||
};
|
||||
loop_chargers(&mut ctx, &args.charger_id, cmd)?
|
||||
}
|
||||
}
|
||||
|
||||
for c in &chargers {
|
||||
match args.mode {
|
||||
Mode::Status => {
|
||||
println!("{}: {:?}", c.id, c.state(&mut ctx));
|
||||
},
|
||||
Mode::Session { session: Session::Ongoing }=> {
|
||||
show_session(&c.ongoing_session(&mut ctx)?);
|
||||
},
|
||||
Mode::Session { session: Session::Latest } => {
|
||||
show_session(&c.latest_session(&mut ctx)?);
|
||||
},
|
||||
Mode::Charge { command } => {
|
||||
match command {
|
||||
Command::Start => c.start(&mut ctx)?,
|
||||
Command::Stop => c.stop(&mut ctx)?,
|
||||
Command::Pause => c.pause(&mut ctx)?,
|
||||
Command::Resume => c.resume(&mut ctx)?,
|
||||
}
|
||||
},
|
||||
_other => {
|
||||
unreachable!("Stream was already ruled out above")
|
||||
},
|
||||
}
|
||||
}
|
||||
Mode::Stream => stream(&args.charger_id)?,
|
||||
Mode::Power => {
|
||||
let pow = prom::current_power(&*args.prometheus)?;
|
||||
println!("P1:{}W P2:{}W P3:{}W", pow.0, pow.1, pow.2);
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::write(SAVED_TOKEN_PATH, ctx.save().as_bytes())?;
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
fn show_session(s: &Option<ChargingSession>) {
|
||||
let Some(s) = s.as_ref() else { return };
|
||||
|
||||
let duration = std::time::Duration::from_secs(
|
||||
s.charge_duration_in_seconds.unwrap_or(0) as u64
|
||||
);
|
||||
|
||||
println!("{}\t{}\t{}kWh",
|
||||
let duration = std::time::Duration::from_secs(s.charge_duration_in_seconds.unwrap_or(0) as u64);
|
||||
|
||||
println!(
|
||||
"{}\t{}\t{}kWh",
|
||||
s.charger_id.as_deref().unwrap_or("<none>"),
|
||||
humantime::format_duration(duration),
|
||||
s.session_energy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
73
src/prom.rs
Normal file
73
src/prom.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use ureq::serde::Deserialize;
|
||||
|
||||
use tracing::warn;
|
||||
|
||||
const PROM_QUERY: &str = "/api/v1/query?query=ac_power_watts%7Bmeter%3D%22mains%22%2Cpower%3D%22active%22%2Cphase%21%3D%22total%22%7D";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "status")]
|
||||
enum PromReply {
|
||||
Success {
|
||||
data: PromData,
|
||||
},
|
||||
Error {
|
||||
data: PromData,
|
||||
error_type: String,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "resultType", content = "result")]
|
||||
enum PromData {
|
||||
Vector(Vec<VecEntry>),
|
||||
Matrix(Vec<MatrixEntry>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct VecEntry {
|
||||
metric: HashMap<String, String>,
|
||||
value: (f64, String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MatrixEntry {
|
||||
metric: HashMap<String, String>,
|
||||
values: Vec<(f64, String)>,
|
||||
}
|
||||
|
||||
pub fn current_power(base: &str) -> Result<(f64, f64, f64)> {
|
||||
let url = format!("{}{}", base, PROM_QUERY);
|
||||
|
||||
let reply: PromReply = ureq::get(&url).call()?.into_json()?;
|
||||
|
||||
let PromReply::Success {
|
||||
data: PromData::Vector(v),
|
||||
} = reply
|
||||
else {
|
||||
bail!("Could not understand Prometheus reply: {:?}", reply)
|
||||
};
|
||||
|
||||
let mut r = (None, None, None);
|
||||
|
||||
for entry in &v {
|
||||
let val: f64 = entry.value.1.parse()?;
|
||||
match entry.metric.get("phase").map(|s| &**s) {
|
||||
Some("a") => r.0 = Some(val),
|
||||
Some("b") => r.1 = Some(val),
|
||||
Some("c") => r.2 = Some(val),
|
||||
_ => warn!("Metric with unexpected phase"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
r.0.ok_or_else(|| anyhow!("Missing phase a"))?,
|
||||
r.1.ok_or_else(|| anyhow!("Missing phase b"))?,
|
||||
r.2.ok_or_else(|| anyhow!("Missing phase c"))?,
|
||||
))
|
||||
}
|
Loading…
Reference in New Issue
Block a user