i3status_rs/blocks/battery/
sysfs.rs1use std::convert::Infallible;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use tokio::fs::read_dir;
6use tokio::time::Interval;
7
8use super::{BatteryDevice, BatteryInfo, BatteryStatus, DeviceName};
9use crate::blocks::prelude::*;
10use crate::util::read_file;
11
12make_log_macro!(debug, "battery");
13
14const POWER_SUPPLY_DEVICES_PATH: &str = "/sys/class/power_supply";
16
17#[derive(Copy, Clone, Debug, Eq, PartialEq)]
18enum CapacityLevel {
19 Full,
20 High,
21 Normal,
22 Low,
23 Critical,
24 Unknown,
25}
26
27impl FromStr for CapacityLevel {
28 type Err = Infallible;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 Ok(match s {
32 "Full" => Self::Full,
33 "High" => Self::High,
34 "Normal" => Self::Normal,
35 "Low" => Self::Low,
36 "Critical" => Self::Critical,
37 _ => Self::Unknown,
38 })
39 }
40}
41
42impl CapacityLevel {
43 fn percentage(self) -> Option<f64> {
44 match self {
45 CapacityLevel::Full => Some(100.0),
46 CapacityLevel::High => Some(75.0),
47 CapacityLevel::Normal => Some(50.0),
48 CapacityLevel::Low => Some(25.0),
49 CapacityLevel::Critical => Some(5.0),
50 CapacityLevel::Unknown => None,
51 }
52 }
53}
54
55pub(super) struct Device {
58 dev_name: DeviceName,
59 dev_path: Option<PathBuf>,
60 dev_model: Option<String>,
61 interval: Interval,
62}
63
64impl Device {
65 pub(super) fn new(dev_name: DeviceName, dev_model: Option<String>, interval: Seconds) -> Self {
66 Self {
67 dev_name,
68 dev_path: None,
69 dev_model,
70 interval: interval.timer(),
71 }
72 }
73
74 async fn get_device_path(&mut self) -> Result<Option<&Path>> {
77 if let Some(path) = &self.dev_path {
78 if Self::device_available(path).await {
79 debug!("battery '{}' is still available", path.display());
80 return Ok(self.dev_path.as_deref());
81 }
82 }
83
84 let mut matching_battery = None;
85
86 let mut sysfs_dir = read_dir(POWER_SUPPLY_DEVICES_PATH)
87 .await
88 .error("failed to read /sys/class/power_supply directory")?;
89 while let Some(dir) = sysfs_dir
90 .next_entry()
91 .await
92 .error("failed to read /sys/class/power_supply directory")?
93 {
94 let name = dir.file_name();
95 let name = name.to_str().error("non UTF-8 battery path")?;
96
97 let path = dir.path();
98
99 if !self.dev_name.matches(name)
100 || Self::read_prop::<String>(&path, "type").await.as_deref() != Some("Battery")
101 || !Self::device_available(&path).await
102 {
103 continue;
104 }
105
106 let model_name = Self::read_prop::<String>(&path, "model_name").await;
107 debug!(
108 "battery '{}', model={:?}",
109 path.display(),
110 model_name.as_deref()
111 );
112 if let Some(dev_model) = &self.dev_model {
113 if model_name.as_deref() != Some(dev_model.as_str()) {
114 debug!("Skipping based on model.");
115 continue;
116 }
117 }
118
119 debug!(
120 "Found matching battery: '{}' matches {:?}",
121 path.display(),
122 self.dev_name
123 );
124
125 if name.starts_with("BAT") || name.starts_with("CMB") {
128 return Ok(Some(self.dev_path.insert(path)));
129 } else {
130 matching_battery = Some(path);
131 }
132 }
133
134 Ok(match matching_battery {
135 Some(path) => Some(self.dev_path.insert(path)),
136 None => {
137 debug!("No batteries found");
138 None
139 }
140 })
141 }
142
143 async fn read_prop<T: FromStr + Send + Sync>(path: &Path, prop: &str) -> Option<T> {
144 read_file(path.join(prop))
145 .await
146 .ok()
147 .and_then(|x| x.parse().ok())
148 }
149
150 async fn device_available(path: &Path) -> bool {
151 Self::read_prop::<String>(path, "scope").await.as_deref() == Some("Device")
155 || Self::read_prop::<u8>(path, "present").await == Some(1)
156 }
157}
158
159#[async_trait]
160impl BatteryDevice for Device {
161 async fn get_info(&mut self) -> Result<Option<BatteryInfo>> {
162 let path = match self.get_device_path().await? {
164 Some(path) => path,
165 None => return Ok(None),
166 };
167
168 let (
170 status,
171 capacity_level,
172 capacity,
173 charge_now,
174 charge_full,
175 energy_now,
176 energy_full,
177 power_now,
178 current_now,
179 voltage_now,
180 time_to_empty,
181 time_to_full,
182 ) = tokio::join!(
183 Self::read_prop::<BatteryStatus>(path, "status"),
184 Self::read_prop::<CapacityLevel>(path, "capacity_level"),
185 Self::read_prop::<f64>(path, "capacity"),
186 Self::read_prop::<f64>(path, "charge_now"), Self::read_prop::<f64>(path, "charge_full"), Self::read_prop::<f64>(path, "energy_now"), Self::read_prop::<f64>(path, "energy_full"), Self::read_prop::<f64>(path, "power_now"), Self::read_prop::<f64>(path, "current_now"), Self::read_prop::<f64>(path, "voltage_now"), Self::read_prop::<f64>(path, "time_to_empty"), Self::read_prop::<f64>(path, "time_to_full"), );
196
197 if !Self::device_available(path).await {
198 debug!("battery suddenly unavailable");
201 return Ok(None);
202 }
203
204 debug!("status = {:?}", status);
205 debug!("capacity_level = {:?}", capacity_level);
206 debug!("capacity = {:?}", capacity);
207 debug!("charge_now = {:?}", charge_now);
208 debug!("charge_full = {:?}", charge_full);
209 debug!("energy_now = {:?}", energy_now);
210 debug!("energy_full = {:?}", energy_full);
211 debug!("power_now = {:?}", power_now);
212 debug!("current_now = {:?}", current_now);
213 debug!("voltage_now = {:?}", voltage_now);
214 debug!("time_to_empty = {:?}", time_to_empty);
215 debug!("time_to_full = {:?}", time_to_full);
216
217 let charge_now = charge_now.map(|c| c * 1e-6); let charge_full = charge_full.map(|c| c * 1e-6); let energy_now = energy_now.map(|e| e * 1e-6); let energy_full = energy_full.map(|e| e * 1e-6); let power_now = power_now.map(|e| e * 1e-6); let current_now = current_now.map(|e| e * 1e-6); let voltage_now = voltage_now.map(|e| e * 1e-6); let status = status.unwrap_or_default();
226
227 let calc_capacity = |now, full| Some(now? / full? * 100.0);
230 let capacity = calc_capacity(charge_now, charge_full)
231 .or_else(|| calc_capacity(energy_now, energy_full))
232 .or(capacity)
233 .or_else(|| capacity_level.and_then(CapacityLevel::percentage))
234 .error("Failed to get capacity")?;
235
236 let power = power_now
238 .or_else(|| current_now.zip(voltage_now).map(|(c, v)| c * v))
239 .filter(|&p| p != 0.0);
240
241 let time_remaining = match status {
244 BatteryStatus::Charging =>
245 {
246 #[allow(clippy::unnecessary_lazy_evaluations)]
247 time_to_full.or_else(|| match (energy_now, energy_full, power) {
248 (Some(en), Some(ef), Some(p)) => Some((ef - en) / p * 3600.0),
249 _ => match (charge_now, charge_full, voltage_now, power) {
250 (Some(cn), Some(cf), Some(v), Some(p)) => Some((cf - cn) * v / p * 3600.0),
251 _ => None,
252 },
253 })
254 }
255 BatteryStatus::Discharging =>
256 {
257 #[allow(clippy::unnecessary_lazy_evaluations)]
258 time_to_empty.or_else(|| match (energy_now, power) {
259 (Some(en), Some(p)) => Some(en / p * 3600.0),
260 _ => match (charge_now, voltage_now, power) {
261 (Some(cn), Some(v), Some(p)) => Some(cn * v / p * 3600.0),
262 _ => None,
263 },
264 })
265 }
266 _ => None,
267 };
268
269 Ok(Some(BatteryInfo {
270 status,
271 capacity,
272 power,
273 time_remaining,
274 }))
275 }
276
277 async fn wait_for_change(&mut self) -> Result<()> {
278 self.interval.tick().await;
279 Ok(())
280 }
281}