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 {
|
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)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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;
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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 {
|
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(|_| {
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue