use std::error::Error; use anyhow::{Context as AnyhowContext, Result}; use clap::ValueEnum; use clap::{Parser, Subcommand}; use easee::api::{ApiError, Charger, ChargerState, ChargingSession, Context}; use easee::stream; use tracing::info; mod prom; #[derive(Debug, Clone, Copy, ValueEnum)] enum Command { Start, Stop, Pause, Resume, } #[derive(Debug, Clone, Copy, ValueEnum)] enum Session { Ongoing, Latest, } #[derive(Debug, Clone, Subcommand)] enum Mode { Login, Status, Session { #[arg(default_value = "ongoing")] session: Session, }, Charge { command: Command, }, Stream, Power, } #[derive(Debug, Parser)] struct CLI { #[arg(short, long)] debug: bool, #[arg(short, long)] charger_id: Vec, #[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 { 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> { let chargers: Vec; if names.is_empty() { chargers = ctx.chargers()?; info!("{} chargers available.", chargers.len()); } else { chargers = names .iter() .map(|id| ctx.charger(&id)) .collect::>()?; } 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(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"); } let mut ctx = load_context()?; 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)? } 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)? } 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) { 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", s.charger_id.as_deref().unwrap_or(""), humantime::format_duration(duration), s.session_energy, ) }