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