#![feature(let_chains)] use clap::Parser; use futures::StreamExt; use std::path::PathBuf; mod config; #[derive(clap::Parser, Debug, Clone)] #[clap(author, version, about, long_about = None)] struct Args { #[command(subcommand)] command: Commands, #[clap(long, default_value = "/etc/charge-controller-supervisor/config.json")] config: PathBuf, } #[derive(clap::Subcommand, Debug, Clone)] enum Commands { /// Run charge controller server Watch, /// Print the default config file GenerateConfig, } mod controller; mod gauges; mod pl; mod tristar; mod web; pub const CHARGE_CONTROLLER_LABEL: &str = "charge_controller"; pub const PL_LABEL: &str = "pl_device"; pub const TRISTAR_LABEL: &str = "tristar_device"; #[tokio::main] async fn main() { env_logger::builder() .format_module_path(false) .format_timestamp( if std::env::var("LOG_TIMESTAMP").is_ok_and(|v| v == "false") { None } else { Some(env_logger::TimestampPrecision::Seconds) }, ) .init(); if let Err(e) = run().await { log::error!("{e:?}"); std::process::exit(1); } } async fn run() -> eyre::Result<()> { let args = Args::parse(); match args.command { Commands::Watch => watch(args).await, Commands::GenerateConfig => { let config = config::ConfigStorage::default(); let json = serde_json::to_string_pretty(&config)?; println!("{json}"); Ok(()) } } } async fn watch(args: Args) -> eyre::Result<()> { let _w = config::init_config(&args.config); let (map, follow_voltage_tx, mut controller_tasks) = { let config = config::access_config().await; if config .charge_controllers .iter() .any(|cc| cc.follow_primary && cc.name == config.primary_charge_controller) { return Err(eyre::eyre!( "primary charge controller is set to follow primary!" )); } let mut controllers = Vec::new(); let mut map = std::collections::HashMap::new(); let mut follow_voltage_tx = Vec::new(); for config in &config.charge_controllers { let n = config.name.clone(); match controller::Controller::new(config.clone()) { Ok((v, voltage_tx)) => { map.insert(n, v.get_data_ptr()); controllers.push(v); if let Some(voltage_tx) = voltage_tx { follow_voltage_tx.push(voltage_tx); } } Err(e) => log::error!("couldn't connect to {}: {e:?}", n), } } let follow_voltage_tx = controller::MultiTx(follow_voltage_tx); if let Some(primary) = controllers .iter_mut() .find(|c| c.name() == config.primary_charge_controller) { primary.set_tx_to_secondary(follow_voltage_tx.clone()); } let controller_tasks = futures::stream::FuturesUnordered::new(); for controller in controllers { controller_tasks.push(run_loop(controller)); } (map, follow_voltage_tx, controller_tasks) }; let server = web::rocket(web::ServerState::new( &config::access_config().await.primary_charge_controller, map, follow_voltage_tx, )); let server_task = tokio::task::spawn(server.launch()); tokio::select! { v = controller_tasks.next() => { match v { Some(Err(e)) => { log::error!("{e:?}"); } _ => { log::error!("no controller tasks left???"); } } } v = server_task => { if let Err(e)=v { log::error!("server exited: {e:#?}"); std::process::exit(1); } else { std::process::exit(0); } } } std::process::exit(1) } async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> { let mut timeout = tokio::time::interval(controller.timeout_interval()); loop { if let Some(rx) = controller.get_rx() { tokio::select! { _ = timeout.tick() => { do_refresh(&mut controller).await; } Some(command) = rx.recv() => { if let Err(e) = controller.process_command(command).await { log::error!("controller {} failed to process command: {e}", controller.name()); } } } } else { timeout.tick().await; do_refresh(&mut controller).await; } } } async fn do_refresh(controller: &mut controller::Controller) { if let Err(e) = controller.refresh().await { log::warn!("error reading controller {}: {e:?}", controller.name()); } }