easee-controller/src/main.rs
2024-08-21 22:07:20 +02:00

224 lines
6.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::observation;
use tracing::info;
mod config;
mod control;
mod mattermost;
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,
List,
Status,
Session {
#[arg(default_value = "ongoing")]
session: Session,
},
Charge {
command: Command,
},
Stream,
Power,
Control,
}
#[derive(Debug, Parser)]
struct CLI {
#[arg(short, long)]
debug: bool,
#[arg(short, long)]
charger_id: Vec<String>,
#[arg(short, long, default_value = "./config.json")]
config: 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 mut ctx = easee::api::Context::from_login(&username, &password)?;
info!("Login successful.");
save_context(&mut ctx);
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 ?)")?;
let ctx = easee::api::Context::from_saved(&saved)?.on_refresh(save_context);
Ok(ctx)
}
fn save_context(ctx: &mut Context) {
std::fs::write(SAVED_TOKEN_PATH, ctx.save().as_bytes()).unwrap();
}
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 = observation::Stream::from_context(&mut ctx)?;
let chargers = load_chargers(&mut ctx, names)?;
for c in &chargers {
stream.subscribe(&c.id)?;
}
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");
}
// We need to do this before loading the context
if let Mode::Login = args.mode {
login()?;
return Ok(());
}
let mut ctx = load_context()?;
let config = config::load_config(&args.config)?;
match args.mode {
Mode::Login => login()?,
Mode::List => {
for site in ctx.sites()? {
println!(
"Site {} (level {})",
site.id, site.level_of_access
);
for circuit in site.details(&mut ctx)?.circuits {
println!(" Circuit {} ({}A)", circuit.id, circuit.rated_current);
for charger in circuit.chargers {
println!("Charger {} (level {}", charger.id, charger.level_of_access);
}
}
}
}
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::PromClient::new(config.prometheus.base).current_power()?;
println!("P1:{}W P2:{}W P3:{}W", pow.phase1, pow.phase2, pow.phase3);
}
Mode::Control => {
let chargers = load_chargers(&mut ctx, &args.charger_id)?;
control::start(ctx, config, chargers)?;
}
};
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,
)
}