Skip to main content

ferrix_lib/
battery.rs

1/* battery.rs
2 *
3 * Copyright 2025-2026 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Get information about notebook's battery
22//!
23//! ## Example
24//! ```no-test
25//! use ferrix_lib::battery::BatInfo;
26//! use ferrix_lib::traits::ToJson;
27//!
28//! let bat = BatInfo::new().unwrap();
29//! let bat_json = bat.to_json().unwrap();
30//! dbg!(bat_json);
31//! ```
32
33use anyhow::Result;
34use serde::{Deserialize, Serialize};
35use std::{
36    fs::{read_dir, read_to_string},
37    path::Path,
38};
39
40use crate::traits::ToJson;
41
42/// Information about all installed batteries
43#[derive(Debug, Deserialize, Serialize, Clone)]
44pub struct BatInfo {
45    pub bats: Vec<Battery>,
46}
47
48impl BatInfo {
49    /// Scan `/sys/class/power_supply` and initialize a `BatInfo` instance
50    pub fn new() -> Result<Self> {
51        let mut bats = Vec::new();
52        let base_path = Path::new("/sys/class/power_supply/");
53
54        let dir_contents = read_dir(base_path)?;
55        for dir in dir_contents {
56            let dir = dir?.path();
57            let bat_path = dir.join("type");
58            let bat_type = read_to_string(&bat_path)?;
59            if bat_type.trim() == "Battery" {
60                let uevent_path = dir.join("uevent");
61                if uevent_path.is_file() {
62                    bats.push(Battery::new(uevent_path)?);
63                }
64            } else {
65                continue;
66            }
67        }
68        Ok(Self { bats })
69    }
70}
71
72impl ToJson for BatInfo {}
73
74/// Information from the `uevent` file to a single battery
75#[derive(Debug, Deserialize, Serialize, Clone, Default)]
76pub struct Battery {
77    /// System name of the battery device (e.g. `BAT0`)
78    pub name: Option<String>,
79
80    /// The current charging status of this battery
81    pub status: Option<Status>,
82
83    /// The battery technology type
84    pub technology: Option<String>,
85
86    /// Number of charge cycles the battery has undergone
87    pub cycle_count: Option<usize>,
88
89    /// Minimum design voltage, V
90    pub voltage_min_design: Option<f32>,
91
92    /// Current voltage, V
93    pub voltage_now: Option<f32>,
94
95    /// Current power draw or charge rate, W
96    pub power_now: Option<f32>,
97
98    /// Original design capacity, Wh
99    pub energy_full_design: Option<f32>,
100
101    /// Current maximum capacity the battery can hold, Wh
102    pub energy_full: Option<f32>,
103
104    /// Current remaining energy, Wh
105    pub energy_now: Option<f32>,
106
107    /// Current charge level, % (0..100)
108    pub capacity: Option<u8>,
109
110    /// Qualitative description of the current capacity level
111    pub capacity_level: Option<Level>,
112
113    /// The model name of the battery
114    pub model_name: Option<String>,
115
116    /// The manufacturer of the battery
117    pub manufacturer: Option<String>,
118
119    /// The serial number of the battery
120    pub serial_number: Option<String>,
121
122    /// Supported charge types (e.g. `Standard`, `Fast`, etc.)
123    pub charge_types: Option<String>,
124
125    /// The estimated health of the battery, % (0..100) calculated
126    /// from `energy_full` / `energy_full_design`
127    pub health: Option<f32>,
128
129    /// Estimated time remaining until the battery is empty or full, hours
130    pub estimated_time: Option<f32>,
131
132    /// Estimated time required to fully charge the battery, hours (if
133    /// applicable)
134    pub charge_time: Option<f32>,
135}
136
137impl ToJson for Battery {}
138
139impl Battery {
140    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
141        let contents = read_to_string(&path)?;
142        let lines = contents.lines().map(|line| line.trim());
143        let mut bat = Battery::default();
144
145        for line in lines {
146            let mut chunks = line.split('=');
147            match (chunks.next(), chunks.next()) {
148                (Some(key), Some(val)) => parse_chunks(&mut bat, key, val),
149                _ => continue,
150            }
151        }
152        calculate_time(&mut bat);
153        calculate_health(&mut bat);
154        polish_values(&mut bat);
155
156        Ok(bat)
157    }
158}
159
160fn parse_chunks(bat: &mut Battery, key: &str, val: &str) {
161    let val = val.trim();
162    match key {
163        "POWER_SUPPLY_NAME" => bat.name = Some(val.to_string()),
164        "POWER_SUPPLY_STATUS" => bat.status = Some(Status::from(val)),
165        "POWER_SUPPLY_TECHNOLOGY" => bat.technology = Some(val.to_string()),
166        "POWER_SUPPLY_CYCLE_COUNT" => bat.cycle_count = val.parse().ok(),
167        "POWER_SUPPLY_VOLTAGE_MIN_DESIGN" => bat.voltage_min_design = val.parse().ok(),
168        "POWER_SUPPLY_VOLTAGE_NOW" => bat.voltage_now = val.parse().ok(),
169        "POWER_SUPPLY_POWER_NOW" => bat.power_now = val.parse().ok(),
170        "POWER_SUPPLY_ENERGY_FULL_DESIGN" => bat.energy_full_design = val.parse().ok(),
171        "POWER_SUPPLY_ENERGY_FULL" => bat.energy_full = val.parse().ok(),
172        "POWER_SUPPLY_ENERGY_NOW" => bat.energy_now = val.parse().ok(),
173        "POWER_SUPPLY_CAPACITY" => bat.capacity = val.parse().ok(),
174        "POWER_SUPPLY_CAPACITY_LEVEL" => bat.capacity_level = Some(Level::from(val)),
175        "POWER_SUPPLY_MODEL_NAME" => bat.model_name = Some(val.to_string()),
176        "POWER_SUPPLY_MANUFACTURER" => bat.manufacturer = Some(val.to_string()),
177        "POWER_SUPPLY_SERIAL_NUMBER" => bat.serial_number = Some(val.to_string()),
178        "POWER_SUPPLY_CHARGE_TYPES" => bat.charge_types = Some(val.to_string()),
179        _ => {}
180    }
181}
182
183fn polish_values(bat: &mut Battery) {
184    if let Some(vmd) = bat.voltage_min_design {
185        bat.voltage_min_design = Some(vmd / 1_000_000.);
186    }
187    if let Some(pn) = bat.power_now {
188        bat.power_now = Some(pn / 1_000_000.);
189    }
190    if let Some(vn) = bat.voltage_now {
191        bat.voltage_now = Some(vn / 1_000_000.);
192    }
193    if let Some(efd) = bat.energy_full_design {
194        bat.energy_full_design = Some(efd / 1_000_000.);
195    }
196    if let Some(ef) = bat.energy_full {
197        bat.energy_full = Some(ef / 1_000_000.);
198    }
199    if let Some(en) = bat.energy_now {
200        bat.energy_now = Some(en / 1_000_000.);
201    }
202}
203
204fn calculate_health(bat: &mut Battery) {
205    if let (Some(energy_full), Some(energy_full_design)) = (bat.energy_full, bat.energy_full_design)
206    {
207        bat.health = Some((energy_full / energy_full_design * 100.).min(100.))
208    }
209}
210
211fn calculate_time(bat: &mut Battery) {
212    bat.estimated_time = match (
213        bat.status.as_ref(),
214        bat.energy_now,
215        bat.energy_full,
216        bat.power_now,
217    ) {
218        (Some(Status::Discharging) | Some(Status::NotCharging), Some(now), _, Some(p))
219            if p > 0.001 =>
220        {
221            Some((now / p).clamp(0., 999.))
222        }
223        (Some(Status::Charging), Some(now), Some(full), Some(p)) if p > 0.001 => {
224            Some(((full - now) / p).clamp(0., 999.))
225        }
226        _ => None,
227    }
228}
229
230/// Charging status
231#[derive(Debug, Deserialize, Serialize, Clone, Default)]
232pub enum Status {
233    Full,
234    Discharging,
235    Charging,
236    NotCharging,
237    Unknown(String),
238    #[default]
239    None,
240}
241
242impl From<&str> for Status {
243    fn from(value: &str) -> Self {
244        match value {
245            "Full" => Self::Full,
246            "Discharging" => Self::Discharging,
247            "Charging" => Self::Charging,
248            "Not charging" => Self::NotCharging,
249            _ => Self::Unknown(value.to_string()),
250        }
251    }
252}
253
254/// Capacity level
255#[derive(Debug, Deserialize, Serialize, Clone, Default)]
256pub enum Level {
257    Full,
258    Normal,
259    High,
260    Low,
261    Critical,
262    Unknown(String),
263    #[default]
264    None,
265}
266
267impl From<&str> for Level {
268    fn from(value: &str) -> Self {
269        match value {
270            "Full" => Self::Full,
271            "Normal" => Self::Normal,
272            "High" => Self::High,
273            "Low" => Self::Low,
274            "Critical" => Self::Critical,
275            _ => Self::Unknown(value.to_string()),
276        }
277    }
278}