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 health: Option<f32>,
80    pub discharge_time: Option<f32>,
81    pub charge_time: Option<f32>,
82}
83
84impl ToJson for Battery {}
85
86impl Battery {
87    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
88        let contents = read_to_string(&path)?;
89        let lines = contents.lines().map(|line| line.trim());
90        let mut bat = Battery::default();
91
92        for line in lines {
93            let mut chunks = line.split('=');
94            match (chunks.next(), chunks.next()) {
95                (Some(key), Some(val)) => parse_chunks(&mut bat, key, val),
96                _ => continue,
97            }
98        }
99        polish_values(&mut bat);
100        calculate_health(&mut bat);
101        calculate_time(&mut bat);
102
103        Ok(bat)
104    }
105}
106
107fn parse_chunks(bat: &mut Battery, key: &str, val: &str) {
108    let val = val.trim();
109    match key {
110        "POWER_SUPPLY_NAME" => bat.name = Some(val.to_string()),
111        "POWER_SUPPLY_STATUS" => bat.status = Some(Status::from(val)),
112        "POWER_SUPPLY_TECHNOLOGY" => bat.technology = Some(val.to_string()),
113        "POWER_SUPPLY_CYCLE_COUNT" => bat.cycle_count = val.parse().ok(),
114        "POWER_SUPPLY_VOLTAGE_MIN_DESIGN" => bat.voltage_min_design = val.parse().ok(),
115        "POWER_SUPPLY_VOLTAGE_NOW" => bat.voltage_now = val.parse().ok(),
116        "POWER_SUPPLY_POWER_NOW" => bat.power_now = val.parse().ok(),
117        "POWER_SUPPLY_ENERGY_FULL_DESIGN" => bat.energy_full_design = val.parse().ok(),
118        "POWER_SUPPLY_ENERGY_FULL" => bat.energy_full = val.parse().ok(),
119        "POWER_SUPPLY_ENERGY_NOW" => bat.energy_now = val.parse().ok(),
120        "POWER_SUPPLY_CAPACITY" => bat.capacity = val.parse().ok(),
121        "POWER_SUPPLY_CAPACITY_LEVEL" => bat.capacity_level = Some(Level::from(val)),
122        "POWER_SUPPLY_MODEL_NAME" => bat.model_name = Some(val.to_string()),
123        "POWER_SUPPLY_MANUFACTURER" => bat.manufacturer = Some(val.to_string()),
124        "POWER_SUPPLY_SERIAL_NUMBER" => bat.serial_number = Some(val.to_string()),
125        _ => {}
126    }
127}
128
129fn polish_values(bat: &mut Battery) {
130    if let Some(vmd) = bat.voltage_min_design {
131        bat.voltage_min_design = Some(vmd / 1_000_000.);
132    }
133    if let Some(pn) = bat.power_now {
134        bat.power_now = Some(pn / 1_000_000.);
135    }
136    if let Some(vn) = bat.voltage_now {
137        bat.voltage_now = Some(vn / 1_000_000.);
138    }
139    if let Some(efd) = bat.energy_full_design {
140        bat.energy_full_design = Some(efd / 1_000_000.);
141    }
142    if let Some(ef) = bat.energy_full {
143        bat.energy_full = Some(ef / 1_000_000.);
144    }
145    if let Some(en) = bat.energy_now {
146        bat.energy_now = Some(en / 1_000_000.);
147    }
148}
149
150fn calculate_health(bat: &mut Battery) {
151    if bat.energy_full.is_some() && bat.energy_full_design.is_some() {
152        let (energy_full, energy_full_design) =
153            (bat.energy_full.unwrap(), bat.energy_full_design.unwrap());
154        bat.health = Some((energy_full / energy_full_design * 100.).min(100.));
155    }
156}
157
158fn calculate_time(bat: &mut Battery) {
159    // FIXME!
160    if let (Some(energy_now), Some(power)) = (bat.energy_now, bat.power_now) {
161        if power > 0.001 {
162            bat.discharge_time = Some((energy_now / power).max(0.).min(999.))
163        }
164    }
165
166    if let (Some(energy_now), Some(energy_full), Some(power)) =
167        (bat.energy_now, bat.energy_full, bat.power_now)
168    {
169        if power > 0.001 && energy_full > energy_now {
170            let delta = energy_full - energy_now;
171            let efficiency = 0.85;
172            let eff_power = power * efficiency;
173
174            bat.charge_time = Some((delta / eff_power).max(0.).min(999.));
175        }
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}