273 lines
7.4 KiB
Rust
273 lines
7.4 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, SetCurrent, Triphase};
|
|
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 SiteCommand {
|
|
Set {
|
|
phase1: f64,
|
|
phase2: f64,
|
|
phase3: f64,
|
|
ttl: i32,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Subcommand)]
|
|
enum Mode {
|
|
Login,
|
|
List,
|
|
Status,
|
|
Session {
|
|
#[arg(default_value = "ongoing")]
|
|
session: Session,
|
|
},
|
|
Charge {
|
|
command: Command,
|
|
},
|
|
Site {
|
|
site_id: u32,
|
|
circuit_id: u32,
|
|
#[command(subcommand)]
|
|
cmd: Option<SiteCommand>,
|
|
},
|
|
Stream,
|
|
Power,
|
|
Control,
|
|
}
|
|
|
|
#[derive(Debug, Parser)]
|
|
struct CLI {
|
|
#[arg(short, long)]
|
|
debug: bool,
|
|
|
|
#[arg(short = 'i', 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<()> {
|
|
let stdin = std::io::stdin();
|
|
let mut stderr = std::io::stderr();
|
|
|
|
let mut prompt = |key: &str, pr: &str| -> Result<String > {
|
|
use std::io::Write;
|
|
if let Ok(var) = std::env::var(key) { return Ok(var) }
|
|
let mut buf = String::with_capacity(64);
|
|
write!(stderr, "{pr}: ")?;
|
|
stdin.read_line(&mut buf)?;
|
|
buf.truncate(buf.trim().len());
|
|
Ok(buf)
|
|
};
|
|
|
|
let username = prompt("EASEE_USERNAME", "Username")?;
|
|
let password = prompt("EASEE_PASSWORD", "Password")?;
|
|
|
|
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:.3}kW) 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).context("config file")?;
|
|
|
|
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)?;
|
|
}
|
|
Mode::Site {
|
|
site_id,
|
|
circuit_id,
|
|
cmd,
|
|
} => {
|
|
let site = ctx.circuit(site_id, circuit_id)?;
|
|
match cmd {
|
|
Some(SiteCommand::Set {
|
|
phase1,
|
|
phase2,
|
|
phase3,
|
|
ttl,
|
|
}) => {
|
|
let current = Triphase {
|
|
phase1,
|
|
phase2,
|
|
phase3,
|
|
};
|
|
site.set_dynamic_current(
|
|
&mut ctx,
|
|
SetCurrent {
|
|
time_to_live: Some(ttl),
|
|
current,
|
|
},
|
|
)?;
|
|
}
|
|
None => {
|
|
let current = site.dynamic_current(&mut ctx)?;
|
|
println!("{:?}", current);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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,
|
|
)
|
|
}
|