1use 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#[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#[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 estimated_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 calculate_time(&mut bat);
100 calculate_health(&mut bat);
101 polish_values(&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 bat.estimated_time = match (
160 bat.status.as_ref(),
161 bat.energy_now,
162 bat.energy_full,
163 bat.power_now,
164 ) {
165 (Some(Status::Discharging) | Some(Status::NotCharging), Some(now), _, Some(p))
166 if p > 0.001 =>
167 {
168 Some((now / p).clamp(0., 999.))
169 }
170 (Some(Status::Charging), Some(now), Some(full), Some(p)) if p > 0.001 => {
171 Some(((full - now) / p).clamp(0., 999.))
172 }
173 _ => None,
174 }
175}
176
177#[derive(Debug, Deserialize, Serialize, Clone, Default)]
179pub enum Status {
180 Full,
181 Discharging,
182 Charging,
183 NotCharging,
184 Unknown(String),
185 #[default]
186 None,
187}
188
189impl From<&str> for Status {
190 fn from(value: &str) -> Self {
191 match value {
192 "Full" => Self::Full,
193 "Discharging" => Self::Discharging,
194 "Charging" => Self::Charging,
195 "Not charging" => Self::NotCharging,
196 _ => Self::Unknown(value.to_string()),
197 }
198 }
199}
200
201#[derive(Debug, Deserialize, Serialize, Clone, Default)]
203pub enum Level {
204 Full,
205 Normal,
206 High,
207 Low,
208 Critical,
209 Unknown(String),
210 #[default]
211 None,
212}
213
214impl From<&str> for Level {
215 fn from(value: &str) -> Self {
216 match value {
217 "Full" => Self::Full,
218 "Normal" => Self::Normal,
219 "High" => Self::High,
220 "Low" => Self::Low,
221 "Critical" => Self::Critical,
222 _ => Self::Unknown(value.to_string()),
223 }
224 }
225}