easee-controller/src/main.rs

190 lines
5.0 KiB
Rust

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<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");
}
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<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",
s.charger_id.as_deref().unwrap_or("<none>"),
humantime::format_duration(duration),
s.session_energy,
)
}