mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
battery: clean up, rename to power_supply
This commit is contained in:
parent
87085f913b
commit
d0932ae78c
5 changed files with 190 additions and 295 deletions
267
src/battery.rs
267
src/battery.rs
|
@ -1,267 +0,0 @@
|
|||
use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs};
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub type Result<T, E = ControlError> = std::result::Result<T, E>;
|
||||
|
||||
/// Represents a pattern of path suffixes used to control battery charge thresholds
|
||||
/// for different device vendors.
|
||||
#[derive(Clone)]
|
||||
pub struct ThresholdPathPattern {
|
||||
pub description: &'static str,
|
||||
pub start_path: &'static str,
|
||||
pub stop_path: &'static str,
|
||||
}
|
||||
|
||||
// Threshold patterns
|
||||
const THRESHOLD_PATTERNS: &[ThresholdPathPattern] = &[
|
||||
ThresholdPathPattern {
|
||||
description: "Standard",
|
||||
start_path: "charge_control_start_threshold",
|
||||
stop_path: "charge_control_end_threshold",
|
||||
},
|
||||
ThresholdPathPattern {
|
||||
description: "ASUS",
|
||||
start_path: "charge_control_start_percentage",
|
||||
stop_path: "charge_control_end_percentage",
|
||||
},
|
||||
// Combine Huawei and ThinkPad since they use identical paths
|
||||
ThresholdPathPattern {
|
||||
description: "ThinkPad/Huawei",
|
||||
start_path: "charge_start_threshold",
|
||||
stop_path: "charge_stop_threshold",
|
||||
},
|
||||
// Framework laptop support
|
||||
ThresholdPathPattern {
|
||||
description: "Framework",
|
||||
start_path: "charge_behaviour_start_threshold",
|
||||
stop_path: "charge_behaviour_end_threshold",
|
||||
},
|
||||
];
|
||||
|
||||
/// Represents a battery that supports charge threshold control
|
||||
pub struct SupportedBattery<'a> {
|
||||
pub name: String,
|
||||
pub pattern: &'a ThresholdPathPattern,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// 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)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The thresholds are invalid (start >= stop or stop > 100)
|
||||
/// - No power supply path is found
|
||||
/// - No batteries with threshold support are found
|
||||
/// - Failed to set thresholds on any battery
|
||||
pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> {
|
||||
// Validate thresholds using `BatteryChargeThresholds`
|
||||
let thresholds =
|
||||
BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e {
|
||||
crate::config::types::ConfigError::Validation(msg) => {
|
||||
ControlError::InvalidValueError(msg)
|
||||
}
|
||||
_ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")),
|
||||
})?;
|
||||
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
// XXX: Skip checking directory writability since /sys is a virtual filesystem
|
||||
// Individual file writability will be checked by find_battery_with_threshold_support
|
||||
|
||||
let supported_batteries = find_supported_batteries(power_supply_path)?;
|
||||
if supported_batteries.is_empty() {
|
||||
return Err(ControlError::NotSupported(
|
||||
"No batteries with charge threshold control support found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
apply_thresholds_to_batteries(&supported_batteries, thresholds.start, thresholds.stop)
|
||||
}
|
||||
|
||||
/// Finds all batteries in the system that support threshold control
|
||||
fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBattery<'static>>> {
|
||||
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();
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to read power-supply entry: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let ps_path = entry.path();
|
||||
if is_battery(&ps_path)? {
|
||||
if let Some(battery) = find_battery_with_threshold_support(&ps_path) {
|
||||
supported_batteries.push(battery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if supported_batteries.is_empty() {
|
||||
log::warn!("No batteries with charge threshold support found");
|
||||
} else {
|
||||
log::debug!(
|
||||
"Found {} batteries with threshold support",
|
||||
supported_batteries.len()
|
||||
);
|
||||
for battery in &supported_batteries {
|
||||
log::debug!(
|
||||
"Battery '{}' supports {} threshold control",
|
||||
battery.name,
|
||||
battery.pattern.description
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(supported_batteries)
|
||||
}
|
||||
|
||||
/// Applies the threshold settings to all supported batteries
|
||||
fn apply_thresholds_to_batteries(
|
||||
batteries: &[SupportedBattery<'_>],
|
||||
start_threshold: u8,
|
||||
stop_threshold: u8,
|
||||
) -> Result<()> {
|
||||
let mut errors = Vec::new();
|
||||
let mut success_count = 0;
|
||||
|
||||
for battery in batteries {
|
||||
let start_path = battery.path.join(battery.pattern.start_path);
|
||||
let stop_path = battery.path.join(battery.pattern.stop_path);
|
||||
|
||||
// Read current thresholds in case we need to restore them
|
||||
let current_stop = sysfs::read_sysfs_value(&stop_path).ok();
|
||||
|
||||
// Write stop threshold first (must be >= start threshold)
|
||||
let stop_result = sysfs::write_sysfs_value(&stop_path, &stop_threshold.to_string());
|
||||
|
||||
// Only proceed to set start threshold if stop threshold was set successfully
|
||||
if matches!(stop_result, Ok(())) {
|
||||
let start_result = sysfs::write_sysfs_value(&start_path, &start_threshold.to_string());
|
||||
|
||||
match start_result {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"Set {}-{}% charge thresholds for {} battery '{}'",
|
||||
start_threshold,
|
||||
stop_threshold,
|
||||
battery.pattern.description,
|
||||
battery.name
|
||||
);
|
||||
success_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
// Start threshold failed, try to restore the previous stop threshold
|
||||
if let Some(prev_stop) = ¤t_stop {
|
||||
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
|
||||
if let Err(re) = restore_result {
|
||||
log::warn!(
|
||||
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
|
||||
battery.name,
|
||||
re
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Restored previous stop threshold ({}) for battery '{}'",
|
||||
prev_stop,
|
||||
battery.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
errors.push(format!(
|
||||
"Failed to set start threshold for {} battery '{}': {}",
|
||||
battery.pattern.description, battery.name, e
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if let Err(e) = stop_result {
|
||||
errors.push(format!(
|
||||
"Failed to set stop threshold for {} battery '{}': {}",
|
||||
battery.pattern.description, battery.name, e
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if success_count > 0 {
|
||||
if !errors.is_empty() {
|
||||
log::warn!(
|
||||
"Partial success setting battery thresholds: {}",
|
||||
errors.join("; ")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ControlError::WriteError(format!(
|
||||
"Failed to set charge thresholds on any battery: {}",
|
||||
errors.join("; ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if a power supply entry is a battery
|
||||
fn is_battery(path: &Path) -> Result<bool> {
|
||||
let type_path = path.join("type");
|
||||
|
||||
if !type_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let ps_type = sysfs::read_sysfs_value(&type_path).map_err(|e| {
|
||||
ControlError::ReadError(format!("Failed to read {}: {}", type_path.display(), e))
|
||||
})?;
|
||||
|
||||
Ok(ps_type == "Battery")
|
||||
}
|
||||
|
||||
/// Identifies if a battery supports threshold control and which pattern it uses
|
||||
fn find_battery_with_threshold_support(ps_path: &Path) -> Option<SupportedBattery<'static>> {
|
||||
for pattern in THRESHOLD_PATTERNS {
|
||||
let start_threshold_path = ps_path.join(pattern.start_path);
|
||||
let stop_threshold_path = ps_path.join(pattern.stop_path);
|
||||
|
||||
// Ensure both paths exist and are writable before considering this battery supported
|
||||
if sysfs::path_exists_and_writable(&start_threshold_path)
|
||||
&& sysfs::path_exists_and_writable(&stop_threshold_path)
|
||||
{
|
||||
return Some(SupportedBattery {
|
||||
name: ps_path.file_name()?.to_string_lossy().to_string(),
|
||||
pattern,
|
||||
path: ps_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -15,12 +15,12 @@ macro_rules! default_const {
|
|||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BatteryChargeThresholds {
|
||||
pub struct PowerSupplyChargeThresholds {
|
||||
pub start: u8,
|
||||
pub stop: u8,
|
||||
}
|
||||
|
||||
impl BatteryChargeThresholds {
|
||||
impl PowerSupplyChargeThresholds {
|
||||
pub fn new(start: u8, stop: u8) -> Result<Self, ConfigError> {
|
||||
if stop == 0 {
|
||||
return Err(ConfigError::Validation(
|
||||
|
@ -42,7 +42,7 @@ impl BatteryChargeThresholds {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(u8, u8)> for BatteryChargeThresholds {
|
||||
impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(values: (u8, u8)) -> Result<Self, Self::Error> {
|
||||
|
@ -66,7 +66,7 @@ pub struct ProfileConfig {
|
|||
#[serde(default)]
|
||||
pub enable_auto_turbo: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_charge_thresholds: Option<BatteryChargeThresholds>,
|
||||
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
|
||||
}
|
||||
|
||||
impl Default for ProfileConfig {
|
||||
|
@ -124,7 +124,7 @@ pub struct ProfileConfigToml {
|
|||
#[serde(default = "default_enable_auto_turbo")]
|
||||
pub enable_auto_turbo: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_charge_thresholds: Option<BatteryChargeThresholds>,
|
||||
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
|
||||
|
@ -134,7 +134,7 @@ pub struct AppConfigToml {
|
|||
#[serde(default)]
|
||||
pub battery: ProfileConfigToml,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_charge_thresholds: Option<BatteryChargeThresholds>,
|
||||
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
|
||||
pub ignored_power_supplies: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub daemon: DaemonConfigToml,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::battery;
|
||||
use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
|
||||
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
||||
use crate::cpu::{self};
|
||||
use crate::power_supply;
|
||||
use crate::util::error::{ControlError, EngineError};
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
@ -277,7 +277,7 @@ pub fn determine_and_apply_settings(
|
|||
|
||||
if start_threshold < stop_threshold && stop_threshold <= 100 {
|
||||
log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
|
||||
match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) {
|
||||
match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) {
|
||||
Ok(()) => log::debug!("Battery charge thresholds set successfully"),
|
||||
Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"),
|
||||
}
|
||||
|
|
27
src/main.rs
27
src/main.rs
|
@ -1,4 +1,3 @@
|
|||
mod battery;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod core;
|
||||
|
@ -6,6 +5,7 @@ mod cpu;
|
|||
mod daemon;
|
||||
mod engine;
|
||||
mod monitor;
|
||||
mod power_supply;
|
||||
mod util;
|
||||
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
|
@ -148,27 +148,14 @@ fn real_main() -> anyhow::Result<()> {
|
|||
cpu::set_platform_profile(platform_profile)?;
|
||||
}
|
||||
|
||||
// TODO: This is like this because [`cpu`] doesn't expose
|
||||
// a way of setting them individually. Will clean this up
|
||||
// after that is cleaned.
|
||||
if charge_threshold_start.is_some() || charge_threshold_end.is_some() {
|
||||
let charge_threshold_start = charge_threshold_start.ok_or_else(|| {
|
||||
anyhow!("both charge thresholds should be given at the same time")
|
||||
})?;
|
||||
let charge_threshold_end = charge_threshold_end.ok_or_else(|| {
|
||||
anyhow!("both charge thresholds should be given at the same time")
|
||||
})?;
|
||||
|
||||
if charge_threshold_start >= charge_threshold_end {
|
||||
bail!(
|
||||
"charge start threshold (given as {charge_threshold_start}) must be less than stop threshold (given as {charge_threshold_end})"
|
||||
);
|
||||
for power_supply in power_supply::get_power_supplies()? {
|
||||
if let Some(threshold_start) = charge_threshold_start {
|
||||
power_supply::set_charge_threshold_start(&power_supply, threshold_start)?;
|
||||
}
|
||||
|
||||
battery::set_battery_charge_thresholds(
|
||||
charge_threshold_start,
|
||||
charge_threshold_end,
|
||||
)?;
|
||||
if let Some(threshold_end) = charge_threshold_end {
|
||||
power_supply::set_charge_threshold_end(&power_supply, threshold_end)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
175
src/power_supply.rs
Normal file
175
src/power_supply.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use anyhow::Context;
|
||||
|
||||
use std::{
|
||||
fmt, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Represents a pattern of path suffixes used to control charge thresholds
|
||||
/// for different device vendors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PowerSupplyConfig {
|
||||
pub manufacturer: &'static str,
|
||||
pub path_start: &'static str,
|
||||
pub path_end: &'static str,
|
||||
}
|
||||
|
||||
/// Charge threshold configs.
|
||||
const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[
|
||||
PowerSupplyConfig {
|
||||
manufacturer: "Standard",
|
||||
path_start: "charge_control_start_threshold",
|
||||
path_end: "charge_control_end_threshold",
|
||||
},
|
||||
PowerSupplyConfig {
|
||||
manufacturer: "ASUS",
|
||||
path_start: "charge_control_start_percentage",
|
||||
path_end: "charge_control_end_percentage",
|
||||
},
|
||||
// Combine Huawei and ThinkPad since they use identical paths.
|
||||
PowerSupplyConfig {
|
||||
manufacturer: "ThinkPad/Huawei",
|
||||
path_start: "charge_start_threshold",
|
||||
path_end: "charge_stop_threshold",
|
||||
},
|
||||
// Framework laptop support.
|
||||
PowerSupplyConfig {
|
||||
manufacturer: "Framework",
|
||||
path_start: "charge_behaviour_start_threshold",
|
||||
path_end: "charge_behaviour_end_threshold",
|
||||
},
|
||||
];
|
||||
|
||||
/// Represents a power supply that supports charge threshold control.
|
||||
pub struct PowerSupply {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub config: PowerSupplyConfig,
|
||||
}
|
||||
|
||||
impl fmt::Display for PowerSupply {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"power suppply '{name}' from manufacturer '{manufacturer}'",
|
||||
name = &self.name,
|
||||
manufacturer = &self.config.manufacturer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PowerSupply {
|
||||
pub fn charge_threshold_path_start(&self) -> PathBuf {
|
||||
self.path.join(self.config.path_start)
|
||||
}
|
||||
|
||||
pub fn charge_threshold_path_end(&self) -> PathBuf {
|
||||
self.path.join(self.config.path_end)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Migrate to central utils file. Same exists in cpu.rs.
|
||||
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
fs::write(path, value).with_context(|| {
|
||||
format!(
|
||||
"failed to write '{value}' to '{path}'",
|
||||
path = path.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_power_supply(path: &Path) -> anyhow::Result<bool> {
|
||||
let type_path = path.join("type");
|
||||
|
||||
let type_ = fs::read_to_string(&type_path)
|
||||
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?;
|
||||
|
||||
Ok(type_ == "Battery")
|
||||
}
|
||||
|
||||
/// Get all batteries in the system that support threshold control.
|
||||
pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
|
||||
const PATH: &str = "/sys/class/power_supply";
|
||||
|
||||
let mut power_supplies = Vec::new();
|
||||
|
||||
'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{PATH}'"))? {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
|
||||
Err(error) => {
|
||||
log::warn!("failed to read power supply entry: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let entry_path = entry.path();
|
||||
|
||||
if !is_power_supply(&entry_path).with_context(|| {
|
||||
format!(
|
||||
"failed to determine whether if '{path}' is a power supply",
|
||||
path = entry_path.display(),
|
||||
)
|
||||
})? {
|
||||
continue;
|
||||
}
|
||||
|
||||
for config in POWER_SUPPLY_CONFIGS {
|
||||
if entry_path.join(config.path_start).exists()
|
||||
&& entry_path.join(config.path_end).exists()
|
||||
{
|
||||
power_supplies.push(PowerSupply {
|
||||
name: entry_path
|
||||
.file_name()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to get file name of '{path}'",
|
||||
path = entry_path.display(),
|
||||
)
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
|
||||
path: entry_path,
|
||||
|
||||
config: *config,
|
||||
});
|
||||
continue 'entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(power_supplies)
|
||||
}
|
||||
|
||||
pub fn set_charge_threshold_start(
|
||||
power_supply: &PowerSupply,
|
||||
charge_threshold_start: u8,
|
||||
) -> anyhow::Result<()> {
|
||||
write(
|
||||
&power_supply.charge_threshold_path_start(),
|
||||
&charge_threshold_start.to_string(),
|
||||
)
|
||||
.with_context(|| format!("failed to set charge threshold start for {power_supply}"))?;
|
||||
|
||||
log::info!("set battery threshold start for {power_supply} to {charge_threshold_start}%");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_charge_threshold_end(
|
||||
power_supply: &PowerSupply,
|
||||
charge_threshold_end: u8,
|
||||
) -> anyhow::Result<()> {
|
||||
write(
|
||||
&power_supply.charge_threshold_path_end(),
|
||||
&charge_threshold_end.to_string(),
|
||||
)
|
||||
.with_context(|| format!("failed to set charge threshold end for {power_supply}"))?;
|
||||
|
||||
log::info!("set battery threshold end for {power_supply} to {charge_threshold_end}%");
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue