Skip to main content

ferrix_lib/
battery.rs

1/* battery.rs
2 *
3 * Copyright 2025 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
23use anyhow::Result;
24use serde::{Deserialize, Serialize};
25use std::{
26    fs::{read_dir, read_to_string},
27    path::Path,
28};
29
30use crate::traits::ToJson;
31
32/// Information about all installed batteries
33#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct BatInfo {
35    pub bats: Vec<Battery>,
36}
37
38impl BatInfo {
39    pub fn new() -> Result<Self> {
40        let mut bats = Vec::new();
41        let base_path = Path::new("/sys/class/power_supply/");
42
43        let dir_contents = read_dir(base_path)?;
44        for dir in dir_contents {
45            let dir = dir?.path();
46            let bat_path = dir.join("type");
47            let bat_type = read_to_string(&bat_path)?;
48            if bat_type.trim() == "Battery" {
49                let uevent_path = dir.join("uevent");
50                if uevent_path.is_file() {
51                    bats.push(Battery::new(uevent_path)?);
52                }
53            } else {
54                continue;
55            }
56        }
57        Ok(Self { bats })
58    }
59}
60
61/// Information from the `uevent` file
62#[derive(Debug, Deserialize, Serialize, Clone, Default)]
63pub struct Battery {
64    pub name: Option<String>,
65    pub status: Option<Status>,
66    pub technology: Option<String>,
67    pub cycle_count: Option<usize>,
68    pub voltage_min_design: Option<f32>,
69    pub voltage_now: Option<f32>,
70    pub power_now: Option<f32>,
71    pub energy_full_design: Option<f32>,
72    pub energy_full: Option<f32>,
73    pub energy_now: Option<f32>,
74    pub capacity: Option<u8>,
75    pub capacity_level: Option<Level>,
76    pub model_name: Option<String>,
77    pub manufacturer: Option<String>,
78    pub serial_number: Option<String>,
79    pub charge_types: Option<String>,
80    pub health: Option<f32>,
81    pub estimated_time: Option<f32>,
82    pub charge_time: Option<f32>,
83}
84
85impl ToJson for Battery {}
86
87impl Battery {
88    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
89        let contents = read_to_string(&path)?;
90        let lines = contents.lines().map(|line| line.trim());
91        let mut bat = Battery::default();
92
93        for line in lines {
94            let mut chunks = line.split('=');
95            match (chunks.next(), chunks.next()) {
96                (Some(key), Some(val)) => parse_chunks(&mut bat, key, val),
97                _ => continue,
98            }
99        }
100        calculate_time(&mut bat);
101        calculate_health(&mut bat);
102        polish_values(&mut bat);
103
104        Ok(bat)
105    }
106}
107
108fn parse_chunks(bat: &mut Battery, key: &str, val: &str) {
109    let val = val.trim();
110    match key {
111        "POWER_SUPPLY_NAME" => bat.name = Some(val.to_string()),
112        "POWER_SUPPLY_STATUS" => bat.status = Some(Status::from(val)),
113        "POWER_SUPPLY_TECHNOLOGY" => bat.technology = Some(val.to_string()),
114        "POWER_SUPPLY_CYCLE_COUNT" => bat.cycle_count = val.parse().ok(),
115        "POWER_SUPPLY_VOLTAGE_MIN_DESIGN" => bat.voltage_min_design = val.parse().ok(),
116        "POWER_SUPPLY_VOLTAGE_NOW" => bat.voltage_now = val.parse().ok(),
117        "POWER_SUPPLY_POWER_NOW" => bat.power_now = val.parse().ok(),
118        "POWER_SUPPLY_ENERGY_FULL_DESIGN" => bat.energy_full_design = val.parse().ok(),
119        "POWER_SUPPLY_ENERGY_FULL" => bat.energy_full = val.parse().ok(),
120        "POWER_SUPPLY_ENERGY_NOW" => bat.energy_now = val.parse().ok(),
121        "POWER_SUPPLY_CAPACITY" => bat.capacity = val.parse().ok(),
122        "POWER_SUPPLY_CAPACITY_LEVEL" => bat.capacity_level = Some(Level::from(val)),
123        "POWER_SUPPLY_MODEL_NAME" => bat.model_name = Some(val.to_string()),
124        "POWER_SUPPLY_MANUFACTURER" => bat.manufacturer = Some(val.to_string()),
125        "POWER_SUPPLY_SERIAL_NUMBER" => bat.serial_number = Some(val.to_string()),
126        "POWER_SUPPLY_CHARGE_TYPES" => bat.charge_types = Some(val.to_string()),
127        _ => {}
128    }
129}
130
131fn polish_values(bat: &mut Battery) {
132    if let Some(vmd) = bat.voltage_min_design {
133        bat.voltage_min_design = Some(vmd / 1_000_000.);
134    }
135    if let Some(pn) = bat.power_now {
136        bat.power_now = Some(pn / 1_000_000.);
137    }
138    if let Some(vn) = bat.voltage_now {
139        bat.voltage_now = Some(vn / 1_000_000.);
140    }
141    if let Some(efd) = bat.energy_full_design {
142        bat.energy_full_design = Some(efd / 1_000_000.);
143    }
144    if let Some(ef) = bat.energy_full {
145        bat.energy_full = Some(ef / 1_000_000.);
146    }
147    if let Some(en) = bat.energy_now {
148        bat.energy_now = Some(en / 1_000_000.);
149    }
150}
151
152fn calculate_health(bat: &mut Battery) {
153    if bat.energy_full.is_some() && bat.energy_full_design.is_some() {
154        let (energy_full, energy_full_design) =
155            (bat.energy_full.unwrap(), bat.energy_full_design.unwrap());
156        bat.health = Some((energy_full / energy_full_design * 100.).min(100.));
157    }
158}
159
160fn calculate_time(bat: &mut Battery) {
161    bat.estimated_time = match (
162        bat.status.as_ref(),
163        bat.energy_now,
164        bat.energy_full,
165        bat.power_now,
166    ) {
167        (Some(Status::Discharging) | Some(Status::NotCharging), Some(now), _, Some(p))
168            if p > 0.001 =>
169        {
170            Some((now / p).clamp(0., 999.))
171        }
172        (Some(Status::Charging), Some(now), Some(full), Some(p)) if p > 0.001 => {
173            Some(((full - now) / p).clamp(0., 999.))
174        }
175        _ => None,
176    }
177}
178
179/// Charging status
180#[derive(Debug, Deserialize, Serialize, Clone, Default)]
181pub enum Status {
182    Full,
183    Discharging,
184    Charging,
185    NotCharging,
186    Unknown(String),
187    #[default]
188    None,
189}
190
191impl From<&str> for Status {
192    fn from(value: &str) -> Self {
193        match value {
194            "Full" => Self::Full,
195            "Discharging" => Self::Discharging,
196            "Charging" => Self::Charging,
197            "Not charging" => Self::NotCharging,
198            _ => Self::Unknown(value.to_string()),
199        }
200    }
201}
202
203/// Capacity level
204#[derive(Debug, Deserialize, Serialize, Clone, Default)]
205pub enum Level {
206    Full,
207    Normal,
208    High,
209    Low,
210    Critical,
211    Unknown(String),
212    #[default]
213    None,
214}
215
216impl From<&str> for Level {
217    fn from(value: &str) -> Self {
218        match value {
219            "Full" => Self::Full,
220            "Normal" => Self::Normal,
221            "High" => Self::High,
222            "Low" => Self::Low,
223            "Critical" => Self::Critical,
224            _ => Self::Unknown(value.to_string()),
225        }
226    }
227}