rigtop_core/linux/
battery.rs1use std::path::Path;
8
9use super::{SysRoots, read_i64, read_trimmed, sorted_entries};
10use crate::snapshot::BatteryStats;
11
12pub fn read_battery(roots: &SysRoots) -> Option<BatteryStats> {
14 let class_dir = roots.sys.join("class/power_supply");
15 let batteries: Vec<Reading> = sorted_entries(&class_dir)
16 .iter()
17 .filter_map(|entry| read_one(&class_dir.join(entry)))
18 .collect();
19 if batteries.is_empty() {
20 return None;
21 }
22 let energy_now: Option<f64> = batteries.iter().map(|b| b.energy_now).sum();
25 let energy_full: Option<f64> = batteries.iter().map(|b| b.energy_full).sum();
26 let percent = match (energy_now, energy_full) {
27 (Some(now), Some(full)) if full > 0.0 => Some((100.0 * now / full) as f32),
28 _ => {
29 let known: Vec<f32> = batteries.iter().filter_map(|b| b.capacity).collect();
30 (!known.is_empty()).then(|| known.iter().sum::<f32>() / known.len() as f32)
31 }
32 };
33 let powers: Vec<f32> = batteries.iter().filter_map(|b| b.power_w).collect();
34 let power_w = (!powers.is_empty()).then(|| powers.iter().sum());
35 let status = ["Discharging", "Charging"]
36 .into_iter()
37 .find(|s| batteries.iter().any(|b| b.status == *s))
38 .map(str::to_owned)
39 .unwrap_or_else(|| batteries[0].status.clone());
40 Some(BatteryStats {
41 percent,
42 status,
43 power_w,
44 })
45}
46
47struct Reading {
49 status: String,
50 capacity: Option<f32>,
51 energy_now: Option<f64>,
53 energy_full: Option<f64>,
54 power_w: Option<f32>,
55}
56
57fn read_one(dir: &Path) -> Option<Reading> {
58 if read_trimmed(&dir.join("type"))? != "Battery"
59 || read_trimmed(&dir.join("scope")).is_some_and(|s| s == "Device")
60 {
61 return None;
62 }
63 let value = |name: &str| read_i64(&dir.join(name));
64 let (energy_now, energy_full) = match (value("energy_now"), value("energy_full")) {
67 (Some(now), Some(full)) => (Some(now as f64), Some(full as f64)),
68 _ => (
69 value("charge_now").map(|v| v as f64),
70 value("charge_full").map(|v| v as f64),
71 ),
72 };
73 let power_w = value("power_now")
74 .map(|uw| uw.unsigned_abs() as f32 / 1e6)
75 .or_else(|| {
76 let amps = value("current_now")?.unsigned_abs() as f64 / 1e6;
77 let volts = value("voltage_now")?.unsigned_abs() as f64 / 1e6;
78 Some((amps * volts) as f32)
79 });
80 Some(Reading {
81 status: read_trimmed(&dir.join("status")).unwrap_or_default(),
82 capacity: value("capacity").map(|c| c as f32),
83 energy_now,
84 energy_full,
85 power_w,
86 })
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::linux::testutil::write;
93
94 fn roots(root: &Path) -> SysRoots {
95 SysRoots {
96 sys: root.join("sys"),
97 proc: root.join("proc"),
98 }
99 }
100
101 #[test]
102 fn reads_the_system_battery_and_skips_peripherals() {
103 let tmp = tempfile::tempdir().unwrap();
104 let root = tmp.path();
105 let ps = "sys/class/power_supply";
106 write(root, &format!("{ps}/AC/type"), "Mains\n");
107 write(root, &format!("{ps}/BAT0/type"), "Battery\n");
108 write(root, &format!("{ps}/BAT0/status"), "Discharging\n");
109 write(root, &format!("{ps}/BAT0/capacity"), "72\n");
110 write(root, &format!("{ps}/BAT0/energy_now"), "31500000\n");
111 write(root, &format!("{ps}/BAT0/energy_full"), "43750000\n");
112 write(root, &format!("{ps}/BAT0/power_now"), "12340000\n");
113 write(root, &format!("{ps}/hidpp_battery_0/type"), "Battery\n");
115 write(root, &format!("{ps}/hidpp_battery_0/scope"), "Device\n");
116 write(root, &format!("{ps}/hidpp_battery_0/capacity"), "5\n");
117
118 let battery = read_battery(&roots(root)).unwrap();
119 assert_eq!(battery.status, "Discharging");
120 assert!((battery.percent.unwrap() - 72.0).abs() < 0.1);
121 assert!((battery.power_w.unwrap() - 12.34).abs() < 0.01);
122 assert_eq!(battery.summary(), "72% -12W");
123 }
124
125 #[test]
126 fn charge_and_current_based_drivers() {
127 let tmp = tempfile::tempdir().unwrap();
128 let root = tmp.path();
129 let ps = "sys/class/power_supply";
130 write(root, &format!("{ps}/BAT1/type"), "Battery\n");
131 write(root, &format!("{ps}/BAT1/status"), "Charging\n");
132 write(root, &format!("{ps}/BAT1/charge_now"), "2000000\n");
133 write(root, &format!("{ps}/BAT1/charge_full"), "4000000\n");
134 write(root, &format!("{ps}/BAT1/current_now"), "1500000\n");
135 write(root, &format!("{ps}/BAT1/voltage_now"), "12000000\n");
136 let battery = read_battery(&roots(root)).unwrap();
137 assert!((battery.percent.unwrap() - 50.0).abs() < 0.1);
138 assert!((battery.power_w.unwrap() - 18.0).abs() < 0.01);
139 assert_eq!(battery.summary(), "50% +18W");
140 }
141
142 #[test]
143 fn no_battery_on_a_desktop() {
144 let tmp = tempfile::tempdir().unwrap();
145 write(tmp.path(), "sys/class/power_supply/AC/type", "Mains\n");
146 assert_eq!(read_battery(&roots(tmp.path())), None);
147 let full = BatteryStats {
149 percent: Some(100.0),
150 status: "Full".to_owned(),
151 power_w: Some(0.0),
152 };
153 assert_eq!(full.summary(), "100%");
154 }
155}