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 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#[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#[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}