1use anyhow::Result;
24use serde::{Deserialize, Serialize};
25use std::{fs::{read_to_string, read_dir}, path::Path};
26
27use crate::traits::ToJson;
28
29#[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#[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#[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#[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}