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