diff --git a/tesla-charge-controller/Cargo.toml b/tesla-charge-controller/Cargo.toml index 69c3254..86ad467 100644 --- a/tesla-charge-controller/Cargo.toml +++ b/tesla-charge-controller/Cargo.toml @@ -33,6 +33,7 @@ lazy_static = "1.5.0" rand = "0.8.5" reqwest = { version = "0.12.9", default-features = false, features = [ "rustls-tls", + "json", ] } eyre = "0.6.12" diff --git a/tesla-charge-controller/src/api/http.rs b/tesla-charge-controller/src/api/http.rs index 30153ff..477db16 100644 --- a/tesla-charge-controller/src/api/http.rs +++ b/tesla-charge-controller/src/api/http.rs @@ -83,6 +83,7 @@ impl Vehicle { .timeout(std::time::Duration::from_secs(5)) .send() .await?; + let response: ApiResponseOuter = data.json().await?; let response = response.response; diff --git a/tesla-charge-controller/src/api/mod.rs b/tesla-charge-controller/src/api/mod.rs index 01e7bf5..c3777d9 100644 --- a/tesla-charge-controller/src/api/mod.rs +++ b/tesla-charge-controller/src/api/mod.rs @@ -52,24 +52,26 @@ impl Car { } impl CarState { - pub fn charge_state(&self) -> Option<&ChargeState> { - if self.is_outdated() { + pub async fn charge_state(&self) -> Option<&ChargeState> { + if self.is_outdated().await { None } else { self.charge_state.as_ref() } } - pub fn is_charging(&self) -> bool { - self.charge_state().is_some_and(ChargeState::is_charging) + pub async fn is_charging(&self) -> bool { + self.charge_state() + .await + .is_some_and(ChargeState::is_charging) } pub fn data_age(&self) -> std::time::Duration { std::time::Instant::now().duration_since(self.data_received) } - fn is_outdated(&self) -> bool { - self.data_age() > crate::CONFIG.read().car_state_interval + async fn is_outdated(&self) -> bool { + self.data_age() > crate::config::access_config().await.car_state_interval } } diff --git a/tesla-charge-controller/src/config.rs b/tesla-charge-controller/src/config.rs index 2c8c811..579612c 100644 --- a/tesla-charge-controller/src/config.rs +++ b/tesla-charge-controller/src/config.rs @@ -1,5 +1,114 @@ use serde::{Deserialize, Serialize}; +static CONFIG_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new(); +pub(super) static CONFIG: std::sync::OnceLock<tokio::sync::RwLock<Config>> = + std::sync::OnceLock::new(); + +pub async fn access_config<'a>() -> tokio::sync::RwLockReadGuard<'a, Config> { + CONFIG.get().unwrap().read().await +} + +pub(super) struct ConfigWatcher { + _debouncer: notify_debouncer_mini::Debouncer<notify_debouncer_mini::notify::RecommendedWatcher>, + _handle: tokio::task::JoinHandle<()>, +} + +impl ConfigWatcher { + pub fn new(path: impl AsRef<std::path::Path>) -> Option<Self> { + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + + let mut debouncer = + notify_debouncer_mini::new_debouncer(std::time::Duration::from_secs(1), move |v| { + tx.send(v).unwrap(); + }) + .ok()?; + + debouncer + .watcher() + .watch( + path.as_ref(), + notify_debouncer_mini::notify::RecursiveMode::NonRecursive, + ) + .ok()?; + + let config_path = std::path::PathBuf::from(path.as_ref()); + + let handle = tokio::task::spawn(async move { + loop { + match rx.recv().await { + Some(Ok(_event)) => { + let mut config = Config::load(&config_path); + config.validate(); + + if let Err(e) = overwrite_config(config).await { + log::error!("{e:?}"); + } + } + Some(Err(e)) => log::error!("Error {e:?} from watcher"), + None => {} + } + } + }); + + Some(Self { + _debouncer: debouncer, + _handle: handle, + }) + } +} + +async fn overwrite_config(config: Config) -> eyre::Result<()> { + *CONFIG + .get() + .ok_or(eyre::eyre!("could not get config"))? + .write() + .await = config; + Ok(()) +} + +pub fn init_config(path: impl AsRef<std::path::Path>) -> Option<ConfigWatcher> { + log::trace!("loading config..."); + let config = Config::load_and_save_defaults(&path); + log::trace!("watching config for changes..."); + let config_watcher = ConfigWatcher::new(&path); + let _ = CONFIG_PATH.get_or_init(|| path.as_ref().to_path_buf()); + CONFIG.set(tokio::sync::RwLock::new(config)).unwrap(); + + config_watcher +} + +pub struct ConfigHandle<'a> { + handle: tokio::sync::RwLockWriteGuard<'a, Config>, +} + +impl<'a> core::ops::Deref for ConfigHandle<'a> { + type Target = tokio::sync::RwLockWriteGuard<'a, Config>; + + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +impl std::ops::DerefMut for ConfigHandle<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.handle + } +} + +impl Drop for ConfigHandle<'_> { + fn drop(&mut self) { + if let Err(e) = self.save() { + log::error!("error saving config on drop of handle: {e:?}"); + } + } +} + +pub async fn write_to_config<'a>() -> ConfigHandle<'a> { + ConfigHandle { + handle: CONFIG.get().unwrap().write().await, + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(default)] pub struct Config { @@ -34,20 +143,6 @@ impl Default for Config { } } -impl common::config::ConfigTrait for Config { - fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self { - todo!() - } - - fn save(&self) -> eyre::Result<()> { - todo!() - } - - fn is_valid(&self) -> bool { - todo!() - } -} - #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] #[serde(default)] pub struct PidControls { @@ -67,3 +162,41 @@ impl Default for PidControls { } } } + +impl Config { + fn load(path: impl AsRef<std::path::Path>) -> Self { + serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap() + } + + fn save_to(&self, path: impl AsRef<std::path::Path>) -> eyre::Result<()> { + Ok(serde_json::ser::to_writer_pretty( + std::io::BufWriter::new(std::fs::File::create(path)?), + self, + )?) + } + + fn save(&self) -> eyre::Result<()> { + self.save_to(CONFIG_PATH.get().unwrap()) + } + + fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self { + let mut config = Self::load(&path); + config.validate(); + let result = config.save_to(path); + if let Err(e) = result { + log::error!("Failed to save config: {e:#?}",); + } + config + } + + fn validate(&mut self) { + self.shutoff_voltage = self.shutoff_voltage.clamp(40.0, 60.0); + self.max_rate = self.max_rate.clamp(0, 30); + self.min_rate = self.min_rate.clamp(0, self.max_rate); + // self.duty_cycle_too_high = self.duty_cycle_too_high.clamp(0.0, 1.0); + // self.duty_cycle_too_low = self.duty_cycle_too_low.clamp(0.0, 1.0); + self.pid_controls.proportional_gain = self.pid_controls.proportional_gain.clamp(0.0, 50.0); + self.pid_controls.derivative_gain = self.pid_controls.derivative_gain.clamp(0.0, 50.0); + self.pid_controls.load_divisor = self.pid_controls.load_divisor.clamp(1.0, 50.0); + } +} diff --git a/tesla-charge-controller/src/control.rs b/tesla-charge-controller/src/control.rs index bae5be1..5769d40 100644 --- a/tesla-charge-controller/src/control.rs +++ b/tesla-charge-controller/src/control.rs @@ -25,14 +25,32 @@ impl VehicleController { } } + pub async fn run(&mut self) -> ! { + let (mut car_state_tick, mut pid_loop_tick) = { + let config = crate::config::access_config().await; + let car_state_tick = tokio::time::interval(config.car_state_interval); + let pid_loop_tick = tokio::time::interval(std::time::Duration::from_secs( + config.pid_controls.loop_time_seconds, + )); + (car_state_tick, pid_loop_tick) + }; + loop { + tokio::select! { + _ = car_state_tick.tick() => { self.car.update().await; }, + _ = pid_loop_tick.tick() => { self.run_cycle().await; } + Some(req) = self.requests.recv() => { self.process_requests(req).await; } + } + } + } + pub async fn run_cycle(&mut self) { let age = self.car.state().read().await.data_age(); - if age >= crate::CONFIG.read().car_state_interval { + if age >= crate::config::access_config().await.car_state_interval { self.car.update().await; } match self.control_state { ChargeRateControllerState::Inactive => { - if let Some(state) = self.car.state().read().await.charge_state() { + if let Some(state) = self.car.state().read().await.charge_state().await { if state.is_charging() { self.control_state = ChargeRateControllerState::Charging { rate_amps: state.charge_amps, @@ -44,15 +62,11 @@ impl VehicleController { } } - pub async fn process_requests(&mut self) -> eyre::Result<()> { - while let Some(req) = self.requests.recv().await { - match req { - InterfaceRequest::FlashLights => { - self.car.vehicle().flash_lights().await?; - } - } + pub async fn process_requests(&mut self, req: InterfaceRequest) { + if let Err(e) = match req { + InterfaceRequest::FlashLights => self.car.vehicle().flash_lights().await, + } { + log::error!("failed to execute request: {e:?}"); } - - Ok(()) } } diff --git a/tesla-charge-controller/src/main.rs b/tesla-charge-controller/src/main.rs index 08071e1..42ad162 100644 --- a/tesla-charge-controller/src/main.rs +++ b/tesla-charge-controller/src/main.rs @@ -7,16 +7,13 @@ mod config; mod control; mod server; -static CONFIG: common::config::ConfigSingleton<config::Config> = - common::config::ConfigSingleton::new(); - #[derive(clap::Parser, Debug, Clone)] #[clap(author, version, about, long_about = None)] struct Args { #[command(subcommand)] command: Commands, - #[clap(long, default_value = "/etc/tesla-charge-controller")] - config_dir: PathBuf, + #[clap(long, default_value = "/etc/tesla-charge-controller/config.json")] + config: PathBuf, } #[derive(clap::Subcommand, Debug, Clone)] @@ -29,26 +26,55 @@ enum Commands { #[tokio::main] async fn main() { - common::init_log(); + 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); + let args = Args::parse(); + match args.command { + Commands::Watch => { + if let Err(e) = run(args).await { + log::error!("{e:?}"); + std::process::exit(1); + } + } + Commands::GenerateConfig => { + let config = config::Config::default(); + match serde_json::to_string_pretty(&config) { + Ok(config_json) => { + println!("{config_json}"); + } + Err(e) => { + eprintln!("error serializing config: {e:?}"); + } + } + } } } -async fn run() -> eyre::Result<()> { - let args = Args::parse(); - CONFIG.load(args.config_dir.join("tesla.json"))?; +async fn run(args: Args) -> eyre::Result<()> { + let _config_watcher = config::init_config(args.config); - let car = api::Car::new(&CONFIG.read().car_vin); + let car = { + let config = config::access_config().await; + api::Car::new(&config.car_vin) + }; let car = std::sync::Arc::new(car); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let mut vehicle_controller = control::VehicleController::new(car.clone(), rx); let server = server::launch_server(server::ServerState::new(car, tx)); - server.await; - vehicle_controller.run_cycle().await; + tokio::select! { + () = server => {} + () = vehicle_controller.run() => {} + } Ok(()) } diff --git a/tesla-charge-controller/src/server/mod.rs b/tesla-charge-controller/src/server/mod.rs index fb74bc2..1684acc 100644 --- a/tesla-charge-controller/src/server/mod.rs +++ b/tesla-charge-controller/src/server/mod.rs @@ -11,7 +11,11 @@ use rocket::{ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::UnboundedSender; -use crate::{api::Car, control::InterfaceRequest}; +use crate::{ + api::Car, + config::{access_config, write_to_config}, + control::InterfaceRequest, +}; use self::static_handler::UiStatic; @@ -83,6 +87,7 @@ async fn car_state( .read() .await .charge_state() + .await .ok_or(ServerError::NoData)? .clone(), )) @@ -98,8 +103,8 @@ struct ControlState { #[get("/control-state")] async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> { - let is_charging = state.car.state().read().await.is_charging(); - let config = crate::CONFIG.read(); + let is_charging = state.car.state().read().await.is_charging().await; + let config = access_config().await; let control_enable = config.control_enable; let max_rate = config.max_rate; let min_rate = config.min_rate; @@ -119,29 +124,29 @@ fn flash(state: &State<ServerState>, remote_addr: std::net::IpAddr) { } #[post("/disable-control")] -fn disable_control(remote_addr: std::net::IpAddr) { +async fn disable_control(remote_addr: std::net::IpAddr) { log::warn!("disabling control: {remote_addr:?}"); - crate::CONFIG.write().control_enable = false; + write_to_config().await.control_enable = false; } #[post("/enable-control")] -fn enable_control(remote_addr: std::net::IpAddr) { +async fn enable_control(remote_addr: std::net::IpAddr) { log::warn!("enabling control: {remote_addr:?}"); - crate::CONFIG.write().control_enable = true; + write_to_config().await.control_enable = true; } #[post("/shutoff/voltage/<voltage>")] -fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) { +async fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) { log::warn!("setting shutoff voltage: {remote_addr:?}"); let voltage = voltage.clamp(40., 60.); - crate::CONFIG.write().shutoff_voltage = voltage; + write_to_config().await.shutoff_voltage = voltage; } #[post("/shutoff/time/<time>")] -fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) { +async fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) { log::warn!("setting shutoff time: {remote_addr:?}"); let time = time.clamp(5, 120); - crate::CONFIG.write().shutoff_voltage_time_seconds = time; + write_to_config().await.shutoff_voltage_time_seconds = time; } #[derive(Clone, Copy, Serialize, Deserialize, Debug)] @@ -151,8 +156,8 @@ struct ShutoffStatus { } #[get("/shutoff/status")] -fn shutoff_status() -> Json<ShutoffStatus> { - let config = crate::CONFIG.read(); +async fn shutoff_status() -> Json<ShutoffStatus> { + let config = access_config().await; Json(ShutoffStatus { voltage: config.shutoff_voltage, time: config.shutoff_voltage_time_seconds, @@ -160,46 +165,48 @@ fn shutoff_status() -> Json<ShutoffStatus> { } #[post("/set-max/<limit>")] -fn set_max(limit: i64, remote_addr: std::net::IpAddr) { +async fn set_max(limit: i64, remote_addr: std::net::IpAddr) { log::warn!("setting max: {remote_addr:?}"); - let limit = limit.clamp(crate::CONFIG.read().min_rate, 32); - crate::CONFIG.write().max_rate = limit; + let mut config = write_to_config().await; + let limit = limit.clamp(config.min_rate, 32); + config.max_rate = limit; } #[post("/set-min/<limit>")] -fn set_min(limit: i64, remote_addr: std::net::IpAddr) { +async fn set_min(limit: i64, remote_addr: std::net::IpAddr) { log::warn!("setting min: {remote_addr:?}"); - let limit = limit.clamp(0, crate::CONFIG.read().max_rate); - crate::CONFIG.write().min_rate = limit; + let mut config = write_to_config().await; + let limit = limit.clamp(0, config.max_rate); + config.min_rate = limit; } #[post("/pid-settings/proportional/<gain>")] -fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) { +async fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) { log::warn!("setting proportional gain: {remote_addr:?}"); - crate::CONFIG.write().pid_controls.proportional_gain = gain; + write_to_config().await.pid_controls.proportional_gain = gain; } #[post("/pid-settings/derivative/<gain>")] -fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) { +async fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) { log::warn!("setting derivative gain: {remote_addr:?}"); - crate::CONFIG.write().pid_controls.derivative_gain = gain; + write_to_config().await.pid_controls.derivative_gain = gain; } #[post("/pid-settings/loop_time_seconds/<time>")] -fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) { +async fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) { log::warn!("setting pid loop interval: {remote_addr:?}"); - crate::CONFIG.write().pid_controls.loop_time_seconds = time; + write_to_config().await.pid_controls.loop_time_seconds = time; } #[post("/pid-settings/load_divisor/<divisor>")] -fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) { +async fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) { log::warn!("setting load divisor interval: {remote_addr:?}"); - crate::CONFIG.write().pid_controls.load_divisor = divisor; + write_to_config().await.pid_controls.load_divisor = divisor; } #[get("/pid-settings/status")] -fn pid_settings() -> Json<crate::config::PidControls> { - Json(crate::CONFIG.read().pid_controls) +async fn pid_settings() -> Json<crate::config::PidControls> { + Json(access_config().await.pid_controls) } #[get("/metrics")] diff --git a/webapp/index.html b/tesla-charge-controller/webapp/index.html similarity index 100% rename from webapp/index.html rename to tesla-charge-controller/webapp/index.html diff --git a/webapp/info/index.html b/tesla-charge-controller/webapp/info/index.html similarity index 100% rename from webapp/info/index.html rename to tesla-charge-controller/webapp/info/index.html diff --git a/webapp/pid/index.html b/tesla-charge-controller/webapp/pid/index.html similarity index 100% rename from webapp/pid/index.html rename to tesla-charge-controller/webapp/pid/index.html diff --git a/webapp/regulator/index.html b/tesla-charge-controller/webapp/regulator/index.html similarity index 100% rename from webapp/regulator/index.html rename to tesla-charge-controller/webapp/regulator/index.html diff --git a/webapp/script.js b/tesla-charge-controller/webapp/script.js similarity index 100% rename from webapp/script.js rename to tesla-charge-controller/webapp/script.js diff --git a/webapp/shutoff/index.html b/tesla-charge-controller/webapp/shutoff/index.html similarity index 100% rename from webapp/shutoff/index.html rename to tesla-charge-controller/webapp/shutoff/index.html diff --git a/webapp/style.css b/tesla-charge-controller/webapp/style.css similarity index 100% rename from webapp/style.css rename to tesla-charge-controller/webapp/style.css