1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

power_supply: add more stuff and store them

This commit is contained in:
RGBCube 2025-05-22 19:49:45 +03:00
parent 4763b54c97
commit 004b879672
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
7 changed files with 207 additions and 237 deletions

View file

@ -119,7 +119,7 @@ pub struct PowerDelta {
impl PowerDelta { impl PowerDelta {
pub fn apply(&self) -> anyhow::Result<()> { pub fn apply(&self) -> anyhow::Result<()> {
let power_supplies = match &self.for_ { let mut power_supplies = match &self.for_ {
Some(names) => { Some(names) => {
let mut power_supplies = Vec::with_capacity(names.len()); let mut power_supplies = Vec::with_capacity(names.len());
@ -136,13 +136,13 @@ impl PowerDelta {
.collect(), .collect(),
}; };
for power_supply in power_supplies { for power_supply in &mut power_supplies {
if let Some(threshold_start) = self.charge_threshold_start { if let Some(threshold_start) = self.charge_threshold_start {
power_supply.set_charge_threshold_start(threshold_start)?; power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?;
} }
if let Some(threshold_end) = self.charge_threshold_end { if let Some(threshold_end) = self.charge_threshold_end {
power_supply.set_charge_threshold_end(threshold_end)?; power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?;
} }
} }

View file

@ -13,7 +13,6 @@ pub struct CpuGlobalInfo {
// System-wide CPU settings // System-wide CPU settings
pub epp: Option<String>, // Energy Performance Preference pub epp: Option<String>, // Energy Performance Preference
pub epb: Option<String>, // Energy Performance Bias pub epb: Option<String>, // Energy Performance Bias
pub platform_profile: Option<String>,
pub average_temperature_celsius: Option<f32>, // Average temperature across all cores pub average_temperature_celsius: Option<f32>, // Average temperature across all cores
} }

View file

@ -160,8 +160,8 @@ impl Cpu {
// TODO: Don't read this per CPU. Share the read or // TODO: Don't read this per CPU. Share the read or
// find something in /sys/.../cpu{N} that does it. // find something in /sys/.../cpu{N} that does it.
let content = fs::read("/proc/stat") let content = fs::read("/proc/stat")
.context("/proc/stat does not exist")? .context("failed to read CPU stat")?
.context("failed to read CPU stat")?; .context("/proc/stat does not exist")?;
let cpu_name = format!("cpu{number}", number = self.number); let cpu_name = format!("cpu{number}", number = self.number);
@ -175,44 +175,44 @@ impl Cpu {
self.time_user = stats self.time_user = stats
.next() .next()
.with_context(|| format!("failed to find {self} user time"))? .with_context(|| format!("failed to parse {self} user time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} user time"))?; .with_context(|| format!("failed to find {self} user time"))?;
self.time_nice = stats self.time_nice = stats
.next() .next()
.with_context(|| format!("failed to find {self} nice time"))? .with_context(|| format!("failed to parse {self} nice time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} nice time"))?; .with_context(|| format!("failed to find {self} nice time"))?;
self.time_system = stats self.time_system = stats
.next() .next()
.with_context(|| format!("failed to find {self} system time"))? .with_context(|| format!("failed to parse {self} system time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} system time"))?; .with_context(|| format!("failed to find {self} system time"))?;
self.time_idle = stats self.time_idle = stats
.next() .next()
.with_context(|| format!("failed to find {self} idle time"))? .with_context(|| format!("failed to parse {self} idle time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} idle time"))?; .with_context(|| format!("failed to find {self} idle time"))?;
self.time_iowait = stats self.time_iowait = stats
.next() .next()
.with_context(|| format!("failed to find {self} iowait time"))? .with_context(|| format!("failed to parse {self} iowait time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} iowait time"))?; .with_context(|| format!("failed to find {self} iowait time"))?;
self.time_irq = stats self.time_irq = stats
.next() .next()
.with_context(|| format!("failed to find {self} irq time"))? .with_context(|| format!("failed to parse {self} irq time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} irq time"))?; .with_context(|| format!("failed to find {self} irq time"))?;
self.time_softirq = stats self.time_softirq = stats
.next() .next()
.with_context(|| format!("failed to find {self} softirq time"))? .with_context(|| format!("failed to parse {self} softirq time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} softirq time"))?; .with_context(|| format!("failed to find {self} softirq time"))?;
self.time_steal = stats self.time_steal = stats
.next() .next()
.with_context(|| format!("failed to find {self} steal time"))? .with_context(|| format!("failed to parse {self} steal time"))?
.parse() .parse()
.with_context(|| format!("failed to parse {self} steal time"))?; .with_context(|| format!("failed to find {self} steal time"))?;
Ok(()) Ok(())
} }
@ -221,9 +221,11 @@ impl Cpu {
let Self { number, .. } = *self; let Self { number, .. } = *self;
self.available_governors = 'available_governors: { self.available_governors = 'available_governors: {
let Some(Ok(content)) = fs::read(format!( let Some(content) = fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors"
)) else { ))
.with_context(|| format!("failed to read {self} available governors"))?
else {
break 'available_governors Vec::new(); break 'available_governors Vec::new();
}; };
@ -237,8 +239,8 @@ impl Cpu {
fs::read(format!( fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"
)) ))
.with_context(|| format!("failed to find {self} scaling governor"))? .with_context(|| format!("failed to read {self} scaling governor"))?
.with_context(|| format!("failed to read {self} scaling governor"))?, .with_context(|| format!("failed to find {self} scaling governor"))?,
); );
Ok(()) Ok(())
@ -247,21 +249,21 @@ impl Cpu {
fn rescan_frequency(&mut self) -> anyhow::Result<()> { fn rescan_frequency(&mut self) -> anyhow::Result<()> {
let Self { number, .. } = *self; let Self { number, .. } = *self;
let frequency_khz = fs::read_u64(format!( let frequency_khz = fs::read_n::<u64>(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq"
)) ))
.with_context(|| format!("failed to find {self} frequency"))? .with_context(|| format!("failed to parse {self} frequency"))?
.with_context(|| format!("failed to parse {self} frequency"))?; .with_context(|| format!("failed to find {self} frequency"))?;
let frequency_khz_minimum = fs::read_u64(format!( let frequency_khz_minimum = fs::read_n::<u64>(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
)) ))
.with_context(|| format!("failed to find {self} frequency minimum"))? .with_context(|| format!("failed to parse {self} frequency minimum"))?
.with_context(|| format!("failed to parse {self} frequency"))?; .with_context(|| format!("failed to find {self} frequency"))?;
let frequency_khz_maximum = fs::read_u64(format!( let frequency_khz_maximum = fs::read_n::<u64>(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"
)) ))
.with_context(|| format!("failed to find {self} frequency maximum"))? .with_context(|| format!("failed to parse {self} frequency maximum"))?
.with_context(|| format!("failed to parse {self} frequency"))?; .with_context(|| format!("failed to find {self} frequency"))?;
self.frequency_mhz = Some(frequency_khz / 1000); self.frequency_mhz = Some(frequency_khz / 1000);
self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000);
@ -271,12 +273,12 @@ impl Cpu {
} }
fn rescan_epp(&mut self) -> anyhow::Result<()> { fn rescan_epp(&mut self) -> anyhow::Result<()> {
let Self { number, .. } = self; let Self { number, .. } = *self;
self.available_epps = 'available_epps: { self.available_epps = 'available_epps: {
let Some(Ok(content)) = fs::read(format!( let Some(content) = fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences"
)) else { )).with_context(|| format!("failed to read {self} available EPPs"))? else {
break 'available_epps Vec::new(); break 'available_epps Vec::new();
}; };
@ -290,8 +292,8 @@ impl Cpu {
fs::read(format!( fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"
)) ))
.with_context(|| format!("failed to find {self} EPP"))? .with_context(|| format!("failed to read {self} EPP"))?
.with_context(|| format!("failed to read {self} EPP"))?, .with_context(|| format!("failed to find {self} EPP"))?,
); );
Ok(()) Ok(())
@ -332,8 +334,8 @@ impl Cpu {
fs::read(format!( fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"
)) ))
.with_context(|| format!("failed to find {self} EPB"))? .with_context(|| format!("failed to read {self} EPB"))?
.with_context(|| format!("failed to read {self} EPB"))?, .with_context(|| format!("failed to find {self} EPB"))?,
); );
Ok(()) Ok(())
@ -450,9 +452,11 @@ impl Cpu {
fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
let Self { number, .. } = self; let Self { number, .. } = self;
let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( let Some(minimum_frequency_khz) = fs::read_n::<u64>(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
)) else { ))
.with_context(|| format!("failed to read {self} minimum frequency"))?
else {
// Just let it pass if we can't find anything. // Just let it pass if we can't find anything.
return Ok(()); return Ok(());
}; };
@ -492,9 +496,11 @@ impl Cpu {
fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
let Self { number, .. } = self; let Self { number, .. } = self;
let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"
)) else { ))
.with_context(|| format!("failed to read {self} maximum frequency"))?
else {
// Just let it pass if we can't find anything. // Just let it pass if we can't find anything.
return Ok(()); return Ok(());
}; };
@ -558,15 +564,19 @@ impl Cpu {
bail!("no supported CPU boost control mechanism found"); bail!("no supported CPU boost control mechanism found");
} }
pub fn turbo() -> Option<bool> { pub fn turbo() -> anyhow::Result<Option<bool>> {
if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/intel_pstate/no_turbo") { if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo")
return Some(content == 0); .context("failed to read CPU turbo boost status")?
{
return Ok(Some(content == 0));
} }
if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/cpufreq/boost") { if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost")
return Some(content == 1); .context("failed to read CPU turbo boost status")?
{
return Ok(Some(content == 1));
} }
None Ok(None)
} }
} }

View file

@ -1,4 +1,4 @@
use std::{fs, io, path::Path}; use std::{error, fs, io, path::Path, str};
use anyhow::Context; use anyhow::Context;
@ -15,32 +15,35 @@ pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<fs::ReadDir> {
.with_context(|| format!("failed to read directory '{path}'", path = path.display())) .with_context(|| format!("failed to read directory '{path}'", path = path.display()))
} }
pub fn read(path: impl AsRef<Path>) -> Option<anyhow::Result<String>> { pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
let path = path.as_ref(); let path = path.as_ref();
match fs::read_to_string(path) { match fs::read_to_string(path) {
Ok(string) => Some(Ok(string.trim().to_owned())), Ok(string) => Ok(Some(string.trim().to_owned())),
Err(error) if error.kind() == io::ErrorKind::NotFound => None, Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Some( Err(error) => {
Err(error).with_context(|| format!("failed to read '{path}", path = path.display())), Err(error).with_context(|| format!("failed to read '{path}", path = path.display()))
), }
} }
} }
pub fn read_u64(path: impl AsRef<Path>) -> Option<anyhow::Result<u64>> { pub fn read_n<N: str::FromStr>(path: impl AsRef<Path>) -> anyhow::Result<Option<N>>
where
N::Err: error::Error + Send + Sync + 'static,
{
let path = path.as_ref(); let path = path.as_ref();
match read(path)? { match read(path)? {
Ok(content) => Some(content.trim().parse().with_context(|| { Some(content) => Ok(Some(content.trim().parse().with_context(|| {
format!( format!(
"failed to parse contents of '{path}' as a unsigned number", "failed to parse contents of '{path}' as a unsigned number",
path = path.display(), path = path.display(),
) )
})), })?)),
Err(error) => Some(Err(error)), None => Ok(None),
} }
} }

View file

@ -218,8 +218,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result<Vec<CpuCoreInfo>> {
} }
pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok();
// Calculate average CPU temperature from the core temperatures // Calculate average CPU temperature from the core temperatures
let average_temperature_celsius = if cpu_cores.is_empty() { let average_temperature_celsius = if cpu_cores.is_empty() {
None None
@ -244,120 +242,16 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
// Return the constructed CpuGlobalInfo // Return the constructed CpuGlobalInfo
CpuGlobalInfo { CpuGlobalInfo {
platform_profile,
average_temperature_celsius, average_temperature_celsius,
} }
} }
pub fn get_battery_info(config: &AppConfig) -> anyhow::Result<Vec<BatteryInfo>> { pub fn get_battery_info(config: &AppConfig) -> anyhow::Result<Vec<BatteryInfo>> {
let mut batteries = Vec::new();
let power_supply_path = Path::new("/sys/class/power_supply");
if !power_supply_path.exists() {
return Ok(batteries); // no power supply directory
}
let ignored_supplies = config.ignored_power_supplies.clone().unwrap_or_default();
// Determine overall AC connection status
let mut overall_ac_connected = false;
for entry in fs::read_dir(power_supply_path)? {
let entry = entry?;
let ps_path = entry.path();
let name = entry.file_name().into_string().unwrap_or_default();
// Check for AC adapter type (common names: AC, ACAD, ADP)
if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) {
if ps_type == "Mains"
|| ps_type == "USB_PD_DRP"
|| ps_type == "USB_PD"
|| ps_type == "USB_DCP"
|| ps_type == "USB_CDP"
|| ps_type == "USB_ACA"
{
// USB types can also provide power
if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) {
if online == 1 {
overall_ac_connected = true;
break;
}
}
}
} else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") {
// Fallback for type file missing
if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) {
if online == 1 {
overall_ac_connected = true;
break;
}
}
}
}
// No AC adapter detected but we're on a desktop system // No AC adapter detected but we're on a desktop system
// Default to AC power for desktops // Default to AC power for desktops
if !overall_ac_connected { if !overall_ac_connected {
overall_ac_connected = is_likely_desktop_system(); overall_ac_connected = is_likely_desktop_system();
} }
for entry in fs::read_dir(power_supply_path)? {
let entry = entry?;
let ps_path = entry.path();
let name = entry.file_name().into_string().unwrap_or_default();
if ignored_supplies.contains(&name) {
continue;
}
if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) {
if ps_type == "Battery" {
// Skip peripheral batteries that aren't real laptop batteries
if is_peripheral_battery(&ps_path, &name) {
log::debug!("Skipping peripheral battery: {name}");
continue;
}
let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok();
let capacity_percent = read_sysfs_value::<u8>(ps_path.join("capacity")).ok();
let power_rate_watts = if ps_path.join("power_now").exists() {
read_sysfs_value::<i32>(ps_path.join("power_now")) // uW
.map(|uw| uw as f32 / 1_000_000.0)
.ok()
} else if ps_path.join("current_now").exists()
&& ps_path.join("voltage_now").exists()
{
let current_ua = read_sysfs_value::<i32>(ps_path.join("current_now")).ok(); // uA
let voltage_uv = read_sysfs_value::<i32>(ps_path.join("voltage_now")).ok(); // uV
if let (Some(c), Some(v)) = (current_ua, voltage_uv) {
// Power (W) = (Voltage (V) * Current (A))
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32)
} else {
None
}
} else {
None
};
let charge_start_threshold =
read_sysfs_value::<u8>(ps_path.join("charge_control_start_threshold")).ok();
let charge_stop_threshold =
read_sysfs_value::<u8>(ps_path.join("charge_control_end_threshold")).ok();
batteries.push(BatteryInfo {
name: name.clone(),
ac_connected: overall_ac_connected,
charging_state: status_str,
capacity_percent,
power_rate_watts,
charge_start_threshold,
charge_stop_threshold,
});
}
}
}
// If we found no batteries but have power supplies, we're likely on a desktop // If we found no batteries but have power supplies, we're likely on a desktop
if batteries.is_empty() && overall_ac_connected { if batteries.is_empty() && overall_ac_connected {
log::debug!("No laptop batteries found, likely a desktop system"); log::debug!("No laptop batteries found, likely a desktop system");
@ -366,23 +260,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result<Vec<BatteryInfo>>
Ok(batteries) Ok(batteries)
} }
pub fn collect_system_report(config: &AppConfig) -> anyhow::Result<SystemReport> {
let system_info = get_system_info();
let cpu_cores = get_all_cpu_core_info()?;
let cpu_global = get_cpu_global_info(&cpu_cores);
let batteries = get_battery_info(config)?;
let system_load = get_system_load()?;
Ok(SystemReport {
system_info,
cpu_cores,
cpu_global,
batteries,
system_load,
timestamp: SystemTime::now(),
})
}
pub fn get_cpu_model() -> anyhow::Result<String> { pub fn get_cpu_model() -> anyhow::Result<String> {
let path = Path::new("/proc/cpuinfo"); let path = Path::new("/proc/cpuinfo");
let content = fs::read_to_string(path).map_err(|_| { let content = fs::read_to_string(path).map_err(|_| {

View file

@ -44,16 +44,38 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
]; ];
/// Represents a power supply that supports charge threshold control. /// Represents a power supply that supports charge threshold control.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq)]
pub struct PowerSupply { pub struct PowerSupply {
pub name: String, pub name: String,
pub path: PathBuf, pub path: PathBuf,
pub type_: String,
pub is_from_peripheral: bool, pub is_from_peripheral: bool,
pub charge_state: Option<String>,
pub charge_percent: Option<f64>,
pub charge_threshold_start: f64,
pub charge_threshold_end: f64,
pub drain_rate_watts: Option<f64>,
pub threshold_config: Option<PowerSupplyThresholdConfig>, pub threshold_config: Option<PowerSupplyThresholdConfig>,
} }
impl PowerSupply {
pub fn is_ac(&self) -> bool {
!self.is_from_peripheral
&& matches!(
&*self.type_,
"Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA"
)
|| self.type_.starts_with("AC")
|| self.type_.contains("ACAD")
|| self.type_.contains("ADP")
}
}
impl fmt::Display for PowerSupply { impl fmt::Display for PowerSupply {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "power supply '{name}'", name = self.name.yellow())?; write!(f, "power supply '{name}'", name = self.name.yellow())?;
@ -77,6 +99,15 @@ impl PowerSupply {
let mut power_supply = Self { let mut power_supply = Self {
path: Path::new(POWER_SUPPLY_PATH).join(&name), path: Path::new(POWER_SUPPLY_PATH).join(&name),
name, name,
type_: String::new(),
charge_state: None,
charge_percent: None,
charge_threshold_start: 0.0,
charge_threshold_end: 1.0,
drain_rate_watts: None,
is_from_peripheral: false, is_from_peripheral: false,
@ -99,6 +130,15 @@ impl PowerSupply {
.to_string(), .to_string(),
path, path,
type_: String::new(),
charge_state: None,
charge_percent: None,
charge_threshold_start: 0.0,
charge_threshold_end: 1.0,
drain_rate_watts: None,
is_from_peripheral: false, is_from_peripheral: false,
@ -131,37 +171,18 @@ impl PowerSupply {
Ok(power_supplies) Ok(power_supplies)
} }
fn get_type(&self) -> anyhow::Result<String> {
let type_path = self.path.join("type");
let type_ = fs::read(&type_path)
.with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))?
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?;
Ok(type_)
}
pub fn rescan(&mut self) -> anyhow::Result<()> { pub fn rescan(&mut self) -> anyhow::Result<()> {
if !self.path.exists() { if !self.path.exists() {
bail!("{self} does not exist"); bail!("{self} does not exist");
} }
self.threshold_config = self self.type_ = {
.get_type() let type_path = self.path.join("type");
.with_context(|| format!("failed to determine what type of power supply '{self}' is"))?
.eq("Battery")
.then(|| {
for config in POWER_SUPPLY_THRESHOLD_CONFIGS {
if self.path.join(config.path_start).exists()
&& self.path.join(config.path_end).exists()
{
return Some(*config);
}
}
None fs::read(&type_path)
}) .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?
.flatten(); .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))?
};
self.is_from_peripheral = 'is_from_peripheral: { self.is_from_peripheral = 'is_from_peripheral: {
let name_lower = self.name.to_lowercase(); let name_lower = self.name.to_lowercase();
@ -179,10 +200,9 @@ impl PowerSupply {
} }
// Small capacity batteries are likely not laptop batteries. // Small capacity batteries are likely not laptop batteries.
if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) { if let Some(energy_full) = fs::read_n::<u64>(self.path.join("energy_full"))
let energy_full = energy_full .with_context(|| format!("failed to read the max charge {self} can hold"))?
.with_context(|| format!("failed to read the max charge '{self}' can hold"))?; {
// Most laptop batteries are at least 20,000,000 µWh (20 Wh). // Most laptop batteries are at least 20,000,000 µWh (20 Wh).
// Peripheral batteries are typically much smaller. // Peripheral batteries are typically much smaller.
if energy_full < 10_000_000 { if energy_full < 10_000_000 {
@ -191,10 +211,9 @@ impl PowerSupply {
} }
} }
// Check for model name that indicates a peripheral // Check for model name that indicates a peripheral
if let Some(model) = fs::read(self.path.join("model_name")) { if let Some(model) = fs::read(self.path.join("model_name"))
let model = .with_context(|| format!("failed to read the model name of {self}"))?
model.with_context(|| format!("failed to read the model name of '{self}'"))?; {
if model.contains("bluetooth") || model.contains("wireless") { if model.contains("bluetooth") || model.contains("wireless") {
break 'is_from_peripheral true; break 'is_from_peripheral true;
} }
@ -203,6 +222,53 @@ impl PowerSupply {
false false
}; };
if self.type_ == "Battery" {
self.charge_state = fs::read(self.path.join("status"))
.with_context(|| format!("failed to read {self} charge status"))?;
self.charge_percent = fs::read_n::<u64>(self.path.join("capacity"))
.with_context(|| format!("failed to read {self} charge percent"))?
.map(|percent| percent as f64 / 100.0);
self.charge_threshold_start =
fs::read_n::<u64>(self.path.join("charge_control_start_threshold"))
.with_context(|| format!("failed to read {self} charge threshold start"))?
.map_or(0.0, |percent| percent as f64 / 100.0);
self.charge_threshold_end =
fs::read_n::<u64>(self.path.join("charge_control_end_threshold"))
.with_context(|| format!("failed to read {self} charge threshold end"))?
.map_or(100.0, |percent| percent as f64 / 100.0);
self.drain_rate_watts = match fs::read_n::<i64>(self.path.join("power_now"))
.with_context(|| format!("failed to read {self} power drain"))?
{
Some(drain) => Some(drain as f64),
None => {
let current_ua = fs::read_n::<i32>(self.path.join("current_now"))
.with_context(|| format!("failed to read {self} current"))?;
let voltage_uv = fs::read_n::<i32>(self.path.join("voltage_now"))
.with_context(|| format!("failed to read {self} voltage"))?;
current_ua.zip(voltage_uv).map(|(current, voltage)| {
// Power (W) = Voltage (V) * Current (A)
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
current as f64 * voltage as f64 / 1e12
})
}
};
self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS
.iter()
.find(|config| {
self.path.join(config.path_start).exists()
&& self.path.join(config.path_end).exists()
})
.copied();
}
Ok(()) Ok(())
} }
@ -216,7 +282,10 @@ impl PowerSupply {
.map(|config| self.path.join(config.path_end)) .map(|config| self.path.join(config.path_end))
} }
pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { pub fn set_charge_threshold_start(
&mut self,
charge_threshold_start: f64,
) -> anyhow::Result<()> {
fs::write( fs::write(
&self.charge_threshold_path_start().ok_or_else(|| { &self.charge_threshold_path_start().ok_or_else(|| {
anyhow!( anyhow!(
@ -224,16 +293,18 @@ impl PowerSupply {
name = self.name, name = self.name,
) )
})?, })?,
&charge_threshold_start.to_string(), &((charge_threshold_start * 100.0) as u8).to_string(),
) )
.with_context(|| format!("failed to set charge threshold start for {self}"))?; .with_context(|| format!("failed to set charge threshold start for {self}"))?;
self.charge_threshold_start = charge_threshold_start;
log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); log::info!("set battery threshold start for {self} to {charge_threshold_start}%");
Ok(()) Ok(())
} }
pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> {
fs::write( fs::write(
&self.charge_threshold_path_end().ok_or_else(|| { &self.charge_threshold_path_end().ok_or_else(|| {
anyhow!( anyhow!(
@ -241,26 +312,30 @@ impl PowerSupply {
name = self.name, name = self.name,
) )
})?, })?,
&charge_threshold_end.to_string(), &((charge_threshold_end * 100.0) as u8).to_string(),
) )
.with_context(|| format!("failed to set charge threshold end for {self}"))?; .with_context(|| format!("failed to set charge threshold end for {self}"))?;
self.charge_threshold_end = charge_threshold_end;
log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); log::info!("set battery threshold end for {self} to {charge_threshold_end}%");
Ok(()) Ok(())
} }
pub fn get_available_platform_profiles() -> Vec<String> { pub fn get_available_platform_profiles() -> anyhow::Result<Vec<String>> {
let path = "/sys/firmware/acpi/platform_profile_choices"; let path = "/sys/firmware/acpi/platform_profile_choices";
let Some(Ok(content)) = fs::read(path) else { let Some(content) =
return Vec::new(); fs::read(path).context("failed to read available ACPI platform profiles")?
else {
return Ok(Vec::new());
}; };
content Ok(content
.split_whitespace() .split_whitespace()
.map(ToString::to_string) .map(ToString::to_string)
.collect() .collect())
} }
/// Sets the platform profile. /// Sets the platform profile.
@ -270,7 +345,7 @@ impl PowerSupply {
/// ///
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html> /// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> {
let profiles = Self::get_available_platform_profiles(); let profiles = Self::get_available_platform_profiles()?;
if !profiles if !profiles
.iter() .iter()
@ -285,4 +360,10 @@ impl PowerSupply {
fs::write("/sys/firmware/acpi/platform_profile", profile) fs::write("/sys/firmware/acpi/platform_profile", profile)
.context("this probably means that your system does not support changing ACPI profiles") .context("this probably means that your system does not support changing ACPI profiles")
} }
pub fn platform_profile() -> anyhow::Result<String> {
fs::read("/sys/firmware/acpi/platform_profile")
.context("failed to read platform profile")?
.context("failed to find platform profile")
}
} }

View file

@ -33,9 +33,9 @@ impl System {
} }
fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { fn rescan_is_desktop(&mut self) -> anyhow::Result<()> {
if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { if let Some(chassis_type) =
let chassis_type = chassis_type.context("failed to read chassis type")?; fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?
{
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One
// 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis
@ -86,8 +86,8 @@ impl System {
fn rescan_load_average(&mut self) -> anyhow::Result<()> { fn rescan_load_average(&mut self) -> anyhow::Result<()> {
let content = fs::read("/proc/loadavg") let content = fs::read("/proc/loadavg")
.context("load average file doesn't exist, are you on linux?")? .context("failed to read load average")?
.context("failed to read load average")?; .context("load average file doesn't exist, are you on linux?")?;
let mut parts = content.split_whitespace(); let mut parts = content.split_whitespace();