feat(core): parse_power_state_from_dir reads sysfs power_supply ABI
Pure function takes a directory path and returns OnAc, OnBattery, or Unknown by reading `type` and `online` files in each supply entry. Matches the kernel ABI documented at Documentation/ABI/testing/sysfs-class-power. Failure modes (missing dir, unreadable files, malformed contents) all fall through to Unknown rather than panicking. Seven tests cover: Mains online, battery-only, USB-PD, empty dir, missing dir, and malformed entries. All pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,61 @@ pub enum PowerState {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Parse a `/sys/class/power_supply/`-style directory and decide
|
||||
/// whether the machine is on AC, on battery, or in an unknown state.
|
||||
///
|
||||
/// Rules (matches `Documentation/ABI/testing/sysfs-class-power`):
|
||||
/// - Any entry with `type` in {`Mains`, `USB`} and `online == 1` → OnAc.
|
||||
/// - Else any entry with `type == Battery` → OnBattery.
|
||||
/// - Else → Unknown.
|
||||
///
|
||||
/// Failures (missing dir, unreadable files, malformed contents) all
|
||||
/// fall through to Unknown rather than panicking. `Unknown` is treated
|
||||
/// as `OnAc` by the caller, which preserves today's pre-clamp
|
||||
/// behaviour on platforms or distros where the probe doesn't fire.
|
||||
pub fn parse_power_state_from_dir(supply_dir: &Path) -> PowerState {
|
||||
let read_dir = match fs::read_dir(supply_dir) {
|
||||
Ok(rd) => rd,
|
||||
Err(_) => return PowerState::Unknown,
|
||||
};
|
||||
|
||||
let mut saw_battery = false;
|
||||
for entry in read_dir.flatten() {
|
||||
let path = entry.path();
|
||||
let ty = read_trimmed(&path.join("type")).unwrap_or_default();
|
||||
let online = read_trimmed(&path.join("online")).unwrap_or_default();
|
||||
match ty.as_str() {
|
||||
"Mains" | "USB" => {
|
||||
if online == "1" {
|
||||
return PowerState::OnAc;
|
||||
}
|
||||
}
|
||||
"Battery" => {
|
||||
saw_battery = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if saw_battery {
|
||||
PowerState::OnBattery
|
||||
} else {
|
||||
PowerState::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn read_trimmed(path: &Path) -> Option<String> {
|
||||
fs::read_to_string(path).ok().map(|s| s.trim().to_owned())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn power_state_variants_are_distinct() {
|
||||
@@ -24,4 +76,56 @@ mod tests {
|
||||
assert_ne!(PowerState::OnAc, PowerState::Unknown);
|
||||
assert_ne!(PowerState::OnBattery, PowerState::Unknown);
|
||||
}
|
||||
|
||||
fn write_supply(dir: &std::path::Path, name: &str, ty: &str, online: &str) {
|
||||
let entry = dir.join(name);
|
||||
fs::create_dir_all(&entry).unwrap();
|
||||
fs::write(entry.join("type"), format!("{ty}\n")).unwrap();
|
||||
fs::write(entry.join("online"), format!("{online}\n")).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_mains_online_as_on_ac() {
|
||||
let dir = tempdir().unwrap();
|
||||
write_supply(dir.path(), "AC", "Mains", "1");
|
||||
write_supply(dir.path(), "BAT0", "Battery", "0");
|
||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnAc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_battery_only_as_on_battery() {
|
||||
let dir = tempdir().unwrap();
|
||||
write_supply(dir.path(), "AC", "Mains", "0");
|
||||
write_supply(dir.path(), "BAT0", "Battery", "0");
|
||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnBattery);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_usb_pd_online_as_on_ac() {
|
||||
let dir = tempdir().unwrap();
|
||||
write_supply(dir.path(), "ucsi-source-psy-USBC000:001", "USB", "1");
|
||||
write_supply(dir.path(), "BAT0", "Battery", "0");
|
||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnAc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_empty_dir_as_unknown() {
|
||||
let dir = tempdir().unwrap();
|
||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_missing_dir_as_unknown() {
|
||||
let path = std::path::Path::new("/no/such/path/should/exist/at/this/inode/123456");
|
||||
assert_eq!(parse_power_state_from_dir(path), PowerState::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_malformed_files_as_unknown_gracefully() {
|
||||
let dir = tempdir().unwrap();
|
||||
let entry = dir.path().join("garbage");
|
||||
fs::create_dir_all(&entry).unwrap();
|
||||
// No type, no online — should not panic.
|
||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user