i3status_rs/blocks/battery/
sysfs.rs

1use 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
14/// Path for the power supply devices
15const 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
55/// Represents a physical power supply device, as known to sysfs.
56/// <https://www.kernel.org/doc/html/v5.15/power/power_supply_class.html>
57pub(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    /// Returns `self.dev_path` if it is still available. Otherwise, find any device that matches
75    /// `self.dev_name`.
76    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            // Better to default to the system battery, rather than possibly a keyboard or mouse battery.
126            // System batteries usually start with BAT or CMB.
127            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        // If `scope` is `Device`, then this is HID, in which case we don't have to check the
152        // `present` property, because the existence of the device directory implies that the device
153        // is available
154        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        // Check if the battery is available
163        let path = match self.get_device_path().await? {
164            Some(path) => path,
165            None => return Ok(None),
166        };
167
168        // Read all the necessary data
169        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"), // uAh
187            Self::read_prop::<f64>(path, "charge_full"), // uAh
188            Self::read_prop::<f64>(path, "energy_now"), // uWh
189            Self::read_prop::<f64>(path, "energy_full"), // uWh
190            Self::read_prop::<f64>(path, "power_now"),  // uW
191            Self::read_prop::<f64>(path, "current_now"), // uA
192            Self::read_prop::<f64>(path, "voltage_now"), // uV
193            Self::read_prop::<f64>(path, "time_to_empty"), // seconds
194            Self::read_prop::<f64>(path, "time_to_full"), // seconds
195        );
196
197        if !Self::device_available(path).await {
198            // Device became unavailable while we were reading data from it. The simplest thing we
199            // can do now is to pretend it wasn't available to begin with.
200            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); // uAh -> Ah
218        let charge_full = charge_full.map(|c| c * 1e-6); // uAh -> Ah
219        let energy_now = energy_now.map(|e| e * 1e-6); // uWh -> Wh
220        let energy_full = energy_full.map(|e| e * 1e-6); // uWh -> Wh
221        let power_now = power_now.map(|e| e * 1e-6); // uW -> W
222        let current_now = current_now.map(|e| e * 1e-6); // uA -> A
223        let voltage_now = voltage_now.map(|e| e * 1e-6); // uV -> V
224
225        let status = status.unwrap_or_default();
226
227        // Prefer `charge_now/charge_full` and `energy_now/energy_full` because `capacity` is
228        // calculated using `_full_design`, which is not practical (#1410, #1906).
229        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        // A * V = W
237        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        // Ah * V = Wh
242        // Wh / W = h
243        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}