1use 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#[derive(Debug, Deserialize, Serialize, Clone)]
44pub struct BatInfo {
45 pub bats: Vec<Battery>,
46}
47
48impl BatInfo {
49 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#[derive(Debug, Deserialize, Serialize, Clone, Default)]
76pub struct Battery {
77 pub name: Option<String>,
79
80 pub status: Option<Status>,
82
83 pub technology: Option<String>,
85
86 pub cycle_count: Option<usize>,
88
89 pub voltage_min_design: Option<f32>,
91
92 pub voltage_now: Option<f32>,
94
95 pub power_now: Option<f32>,
97
98 pub energy_full_design: Option<f32>,
100
101 pub energy_full: Option<f32>,
103
104 pub energy_now: Option<f32>,
106
107 pub capacity: Option<u8>,
109
110 pub capacity_level: Option<Level>,
112
113 pub model_name: Option<String>,
115
116 pub manufacturer: Option<String>,
118
119 pub serial_number: Option<String>,
121
122 pub charge_types: Option<String>,
124
125 pub health: Option<f32>,
128
129 pub estimated_time: Option<f32>,
131
132 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#[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#[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}