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:
parent
4763b54c97
commit
004b879672
7 changed files with 207 additions and 237 deletions
|
@ -119,7 +119,7 @@ pub struct PowerDelta {
|
|||
|
||||
impl PowerDelta {
|
||||
pub fn apply(&self) -> anyhow::Result<()> {
|
||||
let power_supplies = match &self.for_ {
|
||||
let mut power_supplies = match &self.for_ {
|
||||
Some(names) => {
|
||||
let mut power_supplies = Vec::with_capacity(names.len());
|
||||
|
||||
|
@ -136,13 +136,13 @@ impl PowerDelta {
|
|||
.collect(),
|
||||
};
|
||||
|
||||
for power_supply in power_supplies {
|
||||
for power_supply in &mut power_supplies {
|
||||
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 {
|
||||
power_supply.set_charge_threshold_end(threshold_end)?;
|
||||
power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ pub struct CpuGlobalInfo {
|
|||
// System-wide CPU settings
|
||||
pub epp: Option<String>, // Energy Performance Preference
|
||||
pub epb: Option<String>, // Energy Performance Bias
|
||||
pub platform_profile: Option<String>,
|
||||
pub average_temperature_celsius: Option<f32>, // Average temperature across all cores
|
||||
}
|
||||
|
||||
|
|
108
src/cpu.rs
108
src/cpu.rs
|
@ -160,8 +160,8 @@ impl Cpu {
|
|||
// TODO: Don't read this per CPU. Share the read or
|
||||
// find something in /sys/.../cpu{N} that does it.
|
||||
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);
|
||||
|
||||
|
@ -175,44 +175,44 @@ impl Cpu {
|
|||
|
||||
self.time_user = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} user time"))?
|
||||
.with_context(|| format!("failed to parse {self} user time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} user time"))?;
|
||||
.with_context(|| format!("failed to find {self} user time"))?;
|
||||
self.time_nice = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} nice time"))?
|
||||
.with_context(|| format!("failed to parse {self} nice time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} nice time"))?;
|
||||
.with_context(|| format!("failed to find {self} nice time"))?;
|
||||
self.time_system = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} system time"))?
|
||||
.with_context(|| format!("failed to parse {self} system time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} system time"))?;
|
||||
.with_context(|| format!("failed to find {self} system time"))?;
|
||||
self.time_idle = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} idle time"))?
|
||||
.with_context(|| format!("failed to parse {self} idle time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} idle time"))?;
|
||||
.with_context(|| format!("failed to find {self} idle time"))?;
|
||||
self.time_iowait = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} iowait time"))?
|
||||
.with_context(|| format!("failed to parse {self} iowait time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} iowait time"))?;
|
||||
.with_context(|| format!("failed to find {self} iowait time"))?;
|
||||
self.time_irq = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} irq time"))?
|
||||
.with_context(|| format!("failed to parse {self} irq time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} irq time"))?;
|
||||
.with_context(|| format!("failed to find {self} irq time"))?;
|
||||
self.time_softirq = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} softirq time"))?
|
||||
.with_context(|| format!("failed to parse {self} softirq time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} softirq time"))?;
|
||||
.with_context(|| format!("failed to find {self} softirq time"))?;
|
||||
self.time_steal = stats
|
||||
.next()
|
||||
.with_context(|| format!("failed to find {self} steal time"))?
|
||||
.with_context(|| format!("failed to parse {self} steal time"))?
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse {self} steal time"))?;
|
||||
.with_context(|| format!("failed to find {self} steal time"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -221,9 +221,11 @@ impl Cpu {
|
|||
let Self { number, .. } = *self;
|
||||
|
||||
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"
|
||||
)) else {
|
||||
))
|
||||
.with_context(|| format!("failed to read {self} available governors"))?
|
||||
else {
|
||||
break 'available_governors Vec::new();
|
||||
};
|
||||
|
||||
|
@ -237,8 +239,8 @@ impl Cpu {
|
|||
fs::read(format!(
|
||||
"/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(())
|
||||
|
@ -247,21 +249,21 @@ impl Cpu {
|
|||
fn rescan_frequency(&mut self) -> anyhow::Result<()> {
|
||||
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"
|
||||
))
|
||||
.with_context(|| format!("failed to find {self} frequency"))?
|
||||
.with_context(|| format!("failed to parse {self} frequency"))?;
|
||||
let frequency_khz_minimum = fs::read_u64(format!(
|
||||
.with_context(|| format!("failed to parse {self} frequency"))?
|
||||
.with_context(|| format!("failed to find {self} frequency"))?;
|
||||
let frequency_khz_minimum = fs::read_n::<u64>(format!(
|
||||
"/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"))?;
|
||||
let frequency_khz_maximum = fs::read_u64(format!(
|
||||
.with_context(|| format!("failed to parse {self} frequency minimum"))?
|
||||
.with_context(|| format!("failed to find {self} frequency"))?;
|
||||
let frequency_khz_maximum = fs::read_n::<u64>(format!(
|
||||
"/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"))?;
|
||||
.with_context(|| format!("failed to parse {self} frequency maximum"))?
|
||||
.with_context(|| format!("failed to find {self} frequency"))?;
|
||||
|
||||
self.frequency_mhz = Some(frequency_khz / 1000);
|
||||
self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000);
|
||||
|
@ -271,12 +273,12 @@ impl Cpu {
|
|||
}
|
||||
|
||||
fn rescan_epp(&mut self) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
let Self { number, .. } = *self;
|
||||
|
||||
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"
|
||||
)) else {
|
||||
)).with_context(|| format!("failed to read {self} available EPPs"))? else {
|
||||
break 'available_epps Vec::new();
|
||||
};
|
||||
|
||||
|
@ -290,8 +292,8 @@ impl Cpu {
|
|||
fs::read(format!(
|
||||
"/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(())
|
||||
|
@ -332,8 +334,8 @@ impl Cpu {
|
|||
fs::read(format!(
|
||||
"/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(())
|
||||
|
@ -450,9 +452,11 @@ impl Cpu {
|
|||
fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||
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"
|
||||
)) else {
|
||||
))
|
||||
.with_context(|| format!("failed to read {self} minimum frequency"))?
|
||||
else {
|
||||
// Just let it pass if we can't find anything.
|
||||
return Ok(());
|
||||
};
|
||||
|
@ -492,9 +496,11 @@ impl Cpu {
|
|||
fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!(
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||
)) else {
|
||||
let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!(
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"
|
||||
))
|
||||
.with_context(|| format!("failed to read {self} maximum frequency"))?
|
||||
else {
|
||||
// Just let it pass if we can't find anything.
|
||||
return Ok(());
|
||||
};
|
||||
|
@ -558,15 +564,19 @@ impl Cpu {
|
|||
bail!("no supported CPU boost control mechanism found");
|
||||
}
|
||||
|
||||
pub fn turbo() -> Option<bool> {
|
||||
if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/intel_pstate/no_turbo") {
|
||||
return Some(content == 0);
|
||||
pub fn turbo() -> anyhow::Result<Option<bool>> {
|
||||
if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo")
|
||||
.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") {
|
||||
return Some(content == 1);
|
||||
if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost")
|
||||
.context("failed to read CPU turbo boost status")?
|
||||
{
|
||||
return Ok(Some(content == 1));
|
||||
}
|
||||
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
25
src/fs.rs
25
src/fs.rs
|
@ -1,4 +1,4 @@
|
|||
use std::{fs, io, path::Path};
|
||||
use std::{error, fs, io, path::Path, str};
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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).with_context(|| format!("failed to read '{path}", path = path.display())),
|
||||
),
|
||||
Err(error) => {
|
||||
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();
|
||||
|
||||
match read(path)? {
|
||||
Ok(content) => Some(content.trim().parse().with_context(|| {
|
||||
Some(content) => Ok(Some(content.trim().parse().with_context(|| {
|
||||
format!(
|
||||
"failed to parse contents of '{path}' as a unsigned number",
|
||||
path = path.display(),
|
||||
)
|
||||
})),
|
||||
})?)),
|
||||
|
||||
Err(error) => Some(Err(error)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
123
src/monitor.rs
123
src/monitor.rs
|
@ -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 {
|
||||
let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok();
|
||||
|
||||
// Calculate average CPU temperature from the core temperatures
|
||||
let average_temperature_celsius = if cpu_cores.is_empty() {
|
||||
None
|
||||
|
@ -244,120 +242,16 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
|
|||
|
||||
// Return the constructed CpuGlobalInfo
|
||||
CpuGlobalInfo {
|
||||
platform_profile,
|
||||
average_temperature_celsius,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// Default to AC power for desktops
|
||||
if !overall_ac_connected {
|
||||
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 batteries.is_empty() && overall_ac_connected {
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
let path = Path::new("/proc/cpuinfo");
|
||||
let content = fs::read_to_string(path).map_err(|_| {
|
||||
|
|
|
@ -44,16 +44,38 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
|
|||
];
|
||||
|
||||
/// Represents a power supply that supports charge threshold control.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PowerSupply {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
|
||||
pub type_: String,
|
||||
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>,
|
||||
}
|
||||
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "power supply '{name}'", name = self.name.yellow())?;
|
||||
|
@ -77,6 +99,15 @@ impl PowerSupply {
|
|||
let mut power_supply = Self {
|
||||
path: Path::new(POWER_SUPPLY_PATH).join(&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,
|
||||
|
||||
|
@ -99,6 +130,15 @@ impl PowerSupply {
|
|||
.to_string(),
|
||||
|
||||
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,
|
||||
|
||||
|
@ -131,37 +171,18 @@ impl PowerSupply {
|
|||
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<()> {
|
||||
if !self.path.exists() {
|
||||
bail!("{self} does not exist");
|
||||
}
|
||||
|
||||
self.threshold_config = self
|
||||
.get_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);
|
||||
}
|
||||
}
|
||||
self.type_ = {
|
||||
let type_path = self.path.join("type");
|
||||
|
||||
None
|
||||
})
|
||||
.flatten();
|
||||
fs::read(&type_path)
|
||||
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?
|
||||
.with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))?
|
||||
};
|
||||
|
||||
self.is_from_peripheral = 'is_from_peripheral: {
|
||||
let name_lower = self.name.to_lowercase();
|
||||
|
@ -179,10 +200,9 @@ impl PowerSupply {
|
|||
}
|
||||
|
||||
// Small capacity batteries are likely not laptop batteries.
|
||||
if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) {
|
||||
let energy_full = energy_full
|
||||
.with_context(|| format!("failed to read the max charge '{self}' can hold"))?;
|
||||
|
||||
if let Some(energy_full) = fs::read_n::<u64>(self.path.join("energy_full"))
|
||||
.with_context(|| format!("failed to read the max charge {self} can hold"))?
|
||||
{
|
||||
// Most laptop batteries are at least 20,000,000 µWh (20 Wh).
|
||||
// Peripheral batteries are typically much smaller.
|
||||
if energy_full < 10_000_000 {
|
||||
|
@ -191,10 +211,9 @@ impl PowerSupply {
|
|||
}
|
||||
}
|
||||
// Check for model name that indicates a peripheral
|
||||
if let Some(model) = fs::read(self.path.join("model_name")) {
|
||||
let model =
|
||||
model.with_context(|| format!("failed to read the model name of '{self}'"))?;
|
||||
|
||||
if let Some(model) = fs::read(self.path.join("model_name"))
|
||||
.with_context(|| format!("failed to read the model name of {self}"))?
|
||||
{
|
||||
if model.contains("bluetooth") || model.contains("wireless") {
|
||||
break 'is_from_peripheral true;
|
||||
}
|
||||
|
@ -203,6 +222,53 @@ impl PowerSupply {
|
|||
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(())
|
||||
}
|
||||
|
||||
|
@ -216,7 +282,10 @@ impl PowerSupply {
|
|||
.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(
|
||||
&self.charge_threshold_path_start().ok_or_else(|| {
|
||||
anyhow!(
|
||||
|
@ -224,16 +293,18 @@ impl PowerSupply {
|
|||
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}"))?;
|
||||
|
||||
self.charge_threshold_start = charge_threshold_start;
|
||||
|
||||
log::info!("set battery threshold start for {self} to {charge_threshold_start}%");
|
||||
|
||||
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(
|
||||
&self.charge_threshold_path_end().ok_or_else(|| {
|
||||
anyhow!(
|
||||
|
@ -241,26 +312,30 @@ impl PowerSupply {
|
|||
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}"))?;
|
||||
|
||||
self.charge_threshold_end = charge_threshold_end;
|
||||
|
||||
log::info!("set battery threshold end for {self} to {charge_threshold_end}%");
|
||||
|
||||
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 Some(Ok(content)) = fs::read(path) else {
|
||||
return Vec::new();
|
||||
let Some(content) =
|
||||
fs::read(path).context("failed to read available ACPI platform profiles")?
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
content
|
||||
Ok(content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Sets the platform profile.
|
||||
|
@ -270,7 +345,7 @@ impl PowerSupply {
|
|||
///
|
||||
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
||||
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
|
||||
.iter()
|
||||
|
@ -285,4 +360,10 @@ impl PowerSupply {
|
|||
fs::write("/sys/firmware/acpi/platform_profile", profile)
|
||||
.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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,9 +33,9 @@ impl System {
|
|||
}
|
||||
|
||||
fn rescan_is_desktop(&mut self) -> anyhow::Result<()> {
|
||||
if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") {
|
||||
let chassis_type = chassis_type.context("failed to read chassis type")?;
|
||||
|
||||
if let Some(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
|
||||
// 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
|
||||
|
@ -86,8 +86,8 @@ impl System {
|
|||
|
||||
fn rescan_load_average(&mut self) -> anyhow::Result<()> {
|
||||
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();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue