diff --git a/src/cli/debug.rs b/src/cli/debug.rs index c34565d..a36018d 100644 --- a/src/cli/debug.rs +++ b/src/cli/debug.rs @@ -13,7 +13,7 @@ pub fn run_debug(config: &AppConfig) -> Result<(), Box> { println!("Version: {}", env!("CARGO_PKG_VERSION")); // Current date and time - let now = SystemTime::now(); + let _now = SystemTime::now(); // Prefix with underscore to indicate intentionally unused let formatted_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); println!("Timestamp: {formatted_time}"); diff --git a/src/config/load.rs b/src/config/load.rs index f0fedce..1414557 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -68,7 +68,7 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result Result { let toml_app_config = toml::from_str::(&contents).map_err(ConfigError::TomlError)?; + // Handle inheritance of values from global to profile configs + let mut charger_profile = toml_app_config.charger.clone(); + let mut battery_profile = toml_app_config.battery.clone(); + + // If profile-specific battery thresholds are not set, inherit from global config + if charger_profile.battery_charge_thresholds.is_none() { + charger_profile.battery_charge_thresholds = toml_app_config.battery_charge_thresholds; + } + + if battery_profile.battery_charge_thresholds.is_none() { + battery_profile.battery_charge_thresholds = toml_app_config.battery_charge_thresholds; + } + // Convert AppConfigToml to AppConfig Ok(AppConfig { - charger: ProfileConfig::from(toml_app_config.charger), - battery: ProfileConfig::from(toml_app_config.battery), - battery_charge_thresholds: toml_app_config.battery_charge_thresholds, + charger: ProfileConfig::from(charger_profile), + battery: ProfileConfig::from(battery_profile), + global_battery_charge_thresholds: toml_app_config.battery_charge_thresholds, ignored_power_supplies: toml_app_config.ignored_power_supplies, poll_interval_sec: toml_app_config.poll_interval_sec, daemon: DaemonConfig { diff --git a/src/config/types.rs b/src/config/types.rs index 3f4f8ed..83c51b5 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -13,6 +13,7 @@ pub struct ProfileConfig { pub max_freq_mhz: Option, pub platform_profile: Option, pub turbo_auto_settings: Option, + pub battery_charge_thresholds: Option<(u8, u8)>, } impl Default for ProfileConfig { @@ -26,6 +27,7 @@ impl Default for ProfileConfig { max_freq_mhz: None, // no override platform_profile: None, // no override turbo_auto_settings: Some(TurboAutoSettings::default()), + battery_charge_thresholds: None, } } } @@ -36,7 +38,8 @@ pub struct AppConfig { pub charger: ProfileConfig, #[serde(default)] pub battery: ProfileConfig, - pub battery_charge_thresholds: Option<(u8, u8)>, // (start_threshold, stop_threshold) + #[serde(rename = "battery_charge_thresholds")] + pub global_battery_charge_thresholds: Option<(u8, u8)>, // (start_threshold, stop_threshold) pub ignored_power_supplies: Option>, #[serde(default = "default_poll_interval_sec")] pub poll_interval_sec: u64, @@ -92,6 +95,7 @@ pub struct ProfileConfigToml { pub min_freq_mhz: Option, pub max_freq_mhz: Option, pub platform_profile: Option, + pub battery_charge_thresholds: Option<(u8, u8)>, } #[derive(Deserialize, Debug, Clone, Default)] @@ -118,6 +122,7 @@ impl Default for ProfileConfigToml { min_freq_mhz: None, max_freq_mhz: None, platform_profile: None, + battery_charge_thresholds: None, } } } @@ -175,6 +180,7 @@ impl From for ProfileConfig { max_freq_mhz: toml_config.max_freq_mhz, platform_profile: toml_config.platform_profile, turbo_auto_settings: Some(TurboAutoSettings::default()), + battery_charge_thresholds: toml_config.battery_charge_thresholds, } } } diff --git a/src/cpu.rs b/src/cpu.rs index ac8092c..188ff70 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,7 +1,8 @@ use crate::core::{GovernorOverrideMode, TurboSetting}; use crate::util::error::ControlError; use core::str; -use std::{fs, io, path::Path, string::ToString}; +use log::debug; +use std::{fs, io, path::{Path, PathBuf}, string::ToString}; pub type Result = std::result::Result; @@ -342,3 +343,189 @@ pub fn get_governor_override() -> Option { None } } + +/// Set battery charge thresholds to protect battery health +/// +/// This sets the start and stop charging thresholds for batteries that support this feature. +/// Different laptop vendors implement battery thresholds in different ways, so this function +/// attempts to handle multiple implementations (Lenovo, ASUS, etc.). +/// +/// The thresholds determine at what percentage the battery starts charging (when below start_threshold) +/// and at what percentage it stops (when it reaches stop_threshold). +/// +/// # Arguments +/// +/// * `start_threshold` - The battery percentage at which charging should start (typically 0-99) +/// * `stop_threshold` - The battery percentage at which charging should stop (typically 1-100) +/// +pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> { + // Validate threshold values + if start_threshold >= stop_threshold { + return Err(ControlError::InvalidValueError(format!( + "Start threshold ({}) must be less than stop threshold ({})", + start_threshold, stop_threshold + ))); + } + + if stop_threshold > 100 { + return Err(ControlError::InvalidValueError(format!( + "Stop threshold ({}) cannot exceed 100%", + stop_threshold + ))); + } + + // Known sysfs paths for battery threshold control by vendor + let threshold_paths = vec![ + // Standard sysfs paths (used by Lenovo and some others) + ThresholdPathPattern { + description: "Standard", + start_path: "charge_control_start_threshold", + stop_path: "charge_control_end_threshold", + }, + // ASUS-specific paths + ThresholdPathPattern { + description: "ASUS", + start_path: "charge_control_start_percentage", + stop_path: "charge_control_end_percentage", + }, + // Huawei-specific paths + ThresholdPathPattern { + description: "Huawei", + start_path: "charge_start_threshold", + stop_path: "charge_stop_threshold", + }, + ]; + + let power_supply_path = Path::new("/sys/class/power_supply"); + if !power_supply_path.exists() { + return Err(ControlError::NotSupported( + "Power supply path not found, battery threshold control not supported".to_string() + )); + } + + let entries = fs::read_dir(power_supply_path).map_err(|e| { + if e.kind() == io::ErrorKind::PermissionDenied { + ControlError::PermissionDenied(format!( + "Permission denied accessing power supply directory: {}", + power_supply_path.display() + )) + } else { + ControlError::Io(e) + } + })?; + + let mut supported_batteries = Vec::new(); + + // Scan all power supplies for battery threshold support + for entry in entries.flatten() { + let ps_path = entry.path(); + let name = entry.file_name().into_string().unwrap_or_default(); + + // Skip non-battery devices + if !is_battery(&ps_path)? { + continue; + } + + // Try each threshold path pattern for this battery + for pattern in &threshold_paths { + let start_threshold_path = ps_path.join(&pattern.start_path); + let stop_threshold_path = ps_path.join(&pattern.stop_path); + + if start_threshold_path.exists() && stop_threshold_path.exists() { + // Found a battery with threshold support + supported_batteries.push(SupportedBattery { + name: name.clone(), + pattern: pattern.clone(), + path: ps_path.clone(), + }); + + // Found a supported pattern, no need to check others for this battery + break; + } + } + } + + if supported_batteries.is_empty() { + return Err(ControlError::NotSupported( + "No batteries with charge threshold control support found".to_string() + )); + } + + // Apply thresholds to all supported batteries + let mut errors = Vec::new(); + let mut success_count = 0; + + for battery in supported_batteries { + let start_path = battery.path.join(&battery.pattern.start_path); + let stop_path = battery.path.join(&battery.pattern.stop_path); + + // Attempt to set both thresholds + match ( + write_sysfs_value(&start_path, &start_threshold.to_string()), + write_sysfs_value(&stop_path, &stop_threshold.to_string()) + ) { + (Ok(_), Ok(_)) => { + debug!("Set {}-{}% charge thresholds for {} battery '{}'", + start_threshold, stop_threshold, battery.pattern.description, battery.name); + success_count += 1; + }, + (start_result, stop_result) => { + let mut error_msg = format!("Failed to set thresholds for {} battery '{}'", + battery.pattern.description, battery.name); + + if let Err(e) = start_result { + error_msg.push_str(&format!(": start threshold error: {}", e)); + } + if let Err(e) = stop_result { + error_msg.push_str(&format!(": stop threshold error: {}", e)); + } + + errors.push(error_msg); + } + } + } + + if success_count > 0 { + // As long as we successfully set thresholds on at least one battery, consider it a success + if !errors.is_empty() { + debug!("Partial success setting battery thresholds: {}", errors.join("; ")); + } + Ok(()) + } else { + Err(ControlError::WriteError(format!( + "Failed to set charge thresholds on any battery: {}", + errors.join("; ") + ))) + } +} + +/// Helper struct for battery charge threshold path patterns +#[derive(Clone)] +struct ThresholdPathPattern { + description: &'static str, + start_path: &'static str, + stop_path: &'static str, +} + +/// Helper struct for batteries with threshold support +struct SupportedBattery { + name: String, + pattern: ThresholdPathPattern, + path: PathBuf, +} + +/// Check if a power supply entry is a battery +fn is_battery(path: &Path) -> Result { + let type_path = path.join("type"); + + if !type_path.exists() { + return Ok(false); + } + + let ps_type = fs::read_to_string(&type_path) + .map_err(|_| ControlError::ReadError(format!("Failed to read {}", type_path.display())))? + .trim() + .to_string(); + + Ok(ps_type == "Battery") +} diff --git a/src/daemon.rs b/src/daemon.rs index b755226..e7b579a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -202,16 +202,6 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box LevelFilter { - match log_level { - LogLevel::Error => LevelFilter::Error, - LogLevel::Warning => LevelFilter::Warn, - LogLevel::Info => LevelFilter::Info, - LogLevel::Debug => LevelFilter::Debug, - } -} - /// Write current system stats to a file for --stats to read fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { let mut file = File::create(path)?; diff --git a/src/engine.rs b/src/engine.rs index c83d2f2..99cd2f8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,7 +1,7 @@ use crate::config::{AppConfig, ProfileConfig}; use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::cpu::{self}; -use crate::util::error::EngineError; +use crate::util::error::{ControlError, EngineError}; use log::{debug, info}; /// Determines the appropriate CPU profile based on power status or forced mode, @@ -92,6 +92,12 @@ pub fn determine_and_apply_settings( cpu::set_platform_profile(profile)?; } + // Apply battery charge thresholds if configured + apply_battery_charge_thresholds( + selected_profile_config.battery_charge_thresholds, + config.global_battery_charge_thresholds, + )?; + debug!("Profile settings applied successfully."); Ok(()) @@ -186,3 +192,39 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() Err(e) => Err(EngineError::ControlError(e)), } } + +/// Apply battery charge thresholds from configuration +fn apply_battery_charge_thresholds( + profile_thresholds: Option<(u8, u8)>, + global_thresholds: Option<(u8, u8)>, +) -> Result<(), EngineError> { + // Try profile-specific thresholds first, fall back to global thresholds + let thresholds = profile_thresholds.or(global_thresholds); + + if let Some((start_threshold, stop_threshold)) = thresholds { + info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); + match cpu::set_battery_charge_thresholds(start_threshold, stop_threshold) { + Ok(()) => { + debug!("Successfully set battery charge thresholds"); + Ok(()) + } + Err(e) => { + // If the battery doesn't support thresholds, log but don't fail + if matches!(e, ControlError::NotSupported(_)) { + debug!("Battery charge thresholds not supported: {e}"); + Ok(()) + } else { + // For permission errors, provide more helpful message + if matches!(e, ControlError::PermissionDenied(_)) { + debug!("Permission denied setting battery thresholds - requires root privileges"); + } + Err(EngineError::ControlError(e)) + } + } + } + } else { + // No thresholds configured, this is not an error + debug!("No battery charge thresholds configured"); + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 9dfd67e..cf3a4de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,6 +77,13 @@ enum Commands { }, /// Set ACPI platform profile SetPlatformProfile { profile: String }, + /// Set battery charge thresholds to extend battery lifespan + SetBatteryThresholds { + /// Percentage at which charging starts (when below this value) + start_threshold: u8, + /// Percentage at which charging stops (when it reaches this value) + stop_threshold: u8, + }, } fn main() { @@ -358,6 +365,17 @@ fn main() { } Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile) .map_err(|e| Box::new(e) as Box), + Some(Commands::SetBatteryThresholds { + start_threshold, + stop_threshold, + }) => { + info!( + "Setting battery thresholds: start at {}%, stop at {}%", + start_threshold, stop_threshold + ); + cpu::set_battery_charge_thresholds(start_threshold, stop_threshold) + .map_err(|e| Box::new(e) as Box) + } Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), Some(Commands::Debug) => cli::debug::run_debug(&config), None => { diff --git a/src/util/error.rs b/src/util/error.rs index 3ee0a86..63a6628 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -4,6 +4,7 @@ use std::io; pub enum ControlError { Io(io::Error), WriteError(String), + ReadError(String), InvalidValueError(String), NotSupported(String), PermissionDenied(String), @@ -24,6 +25,7 @@ impl std::fmt::Display for ControlError { match self { Self::Io(e) => write!(f, "I/O error: {e}"), Self::WriteError(s) => write!(f, "Failed to write to sysfs path: {s}"), + Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"), Self::InvalidValueError(s) => write!(f, "Invalid value for setting: {s}"), Self::NotSupported(s) => write!(f, "Control action not supported: {s}"), Self::PermissionDenied(s) => {