jolt_platform/linux/
battery.rs1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::Duration;
4
5use color_eyre::eyre::{eyre, Result};
6use starship_battery::units::electric_potential::millivolt;
7use starship_battery::units::energy::watt_hour;
8use starship_battery::units::power::watt;
9use starship_battery::units::ratio::percent;
10use starship_battery::units::thermodynamic_temperature::degree_celsius;
11use starship_battery::units::time::second;
12use starship_battery::Manager;
13
14use crate::battery::{BatteryInfo, BatteryProvider};
15use crate::types::{BatteryTechnology, ChargeState};
16
17const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply";
18
19pub struct LinuxBattery {
20 info: BatteryInfo,
21 manager: Manager,
22 battery_path: Option<PathBuf>,
23}
24
25impl BatteryProvider for LinuxBattery {
26 fn new() -> Result<Self> {
27 let manager = Manager::new()?;
28 let battery_path = find_battery_path();
29 let mut provider = Self {
30 info: BatteryInfo::default(),
31 manager,
32 battery_path,
33 };
34 provider.refresh()?;
35 Ok(provider)
36 }
37
38 fn refresh(&mut self) -> Result<()> {
39 self.refresh_from_battery_crate()?;
40 self.refresh_linux_extras();
41 Ok(())
42 }
43
44 fn info(&self) -> &BatteryInfo {
45 &self.info
46 }
47
48 fn is_supported() -> bool {
49 Path::new(POWER_SUPPLY_PATH).exists()
50 }
51}
52
53impl LinuxBattery {
54 fn refresh_from_battery_crate(&mut self) -> Result<()> {
55 let mut battery = self
56 .manager
57 .batteries()?
58 .next()
59 .ok_or_else(|| eyre!("No battery found"))??;
60
61 self.manager.refresh(&mut battery)?;
62
63 self.info.charge_percent = battery.state_of_charge().get::<percent>();
64 self.info.max_capacity_wh = battery.energy_full().get::<watt_hour>();
65 self.info.design_capacity_wh = battery.energy_full_design().get::<watt_hour>();
66 self.info.voltage_mv = battery.voltage().get::<millivolt>() as u32;
67 self.info.health_percent = battery.state_of_health().get::<percent>();
68 self.info.cycle_count = battery.cycle_count();
69 self.info.temperature_c = battery
70 .temperature()
71 .map(|t| t.get::<degree_celsius>() as f32);
72 self.info.time_to_full = battery
73 .time_to_full()
74 .map(|t| Duration::from_secs(t.get::<second>() as u64));
75 self.info.time_to_empty = battery
76 .time_to_empty()
77 .map(|t| Duration::from_secs(t.get::<second>() as u64));
78
79 let battery_state = battery.state();
80 self.info.state = ChargeState::from(battery_state);
81
82 self.info.vendor = battery.vendor().map(|s| s.to_string());
83 self.info.model = battery.model().map(|s| s.to_string());
84 self.info.serial_number = battery.serial_number().map(|s| s.to_string());
85 self.info.technology = BatteryTechnology::from(battery.technology());
86 self.info.energy_wh = battery.energy().get::<watt_hour>();
87 self.info.energy_rate_watts = battery.energy_rate().get::<watt>();
88
89 Ok(())
90 }
91
92 fn refresh_linux_extras(&mut self) {
93 self.info.external_connected = is_ac_connected();
94
95 if let Some(path) = self.battery_path.clone() {
96 self.detect_not_charging_state(&path);
97 self.read_amperage(&path);
98 }
99 }
100
101 fn detect_not_charging_state(&mut self, battery_path: &Path) {
102 if self.info.state != ChargeState::Unknown {
103 if self.info.external_connected
104 && self.info.state != ChargeState::Charging
105 && self.info.state != ChargeState::Full
106 {
107 self.info.state = ChargeState::NotCharging;
108 }
109 return;
110 }
111
112 let status_path = battery_path.join("status");
113 if let Ok(status) = fs::read_to_string(status_path) {
114 let status = status.trim();
115 if status.eq_ignore_ascii_case("Not charging") {
116 self.info.state = ChargeState::NotCharging;
117 } else if self.info.external_connected {
118 self.info.state = ChargeState::NotCharging;
119 }
120 }
121 }
122
123 fn read_amperage(&mut self, battery_path: &Path) {
124 let current_path = battery_path.join("current_now");
125 if let Ok(content) = fs::read_to_string(current_path) {
126 if let Ok(microamps) = content.trim().parse::<i64>() {
127 let milliamps = (microamps / 1000) as i32;
128 self.info.amperage_ma = if self.info.state == ChargeState::Discharging {
129 -milliamps.abs()
130 } else {
131 milliamps.abs()
132 };
133 }
134 }
135 }
136}
137
138fn find_battery_path() -> Option<PathBuf> {
139 let power_supply = Path::new(POWER_SUPPLY_PATH);
140 if !power_supply.exists() {
141 return None;
142 }
143
144 if let Ok(entries) = fs::read_dir(power_supply) {
145 for entry in entries.flatten() {
146 let path = entry.path();
147 let type_path = path.join("type");
148 if let Ok(type_content) = fs::read_to_string(type_path) {
149 if type_content.trim() == "Battery" {
150 return Some(path);
151 }
152 }
153 }
154 }
155 None
156}
157
158fn is_ac_connected() -> bool {
159 let power_supply = Path::new(POWER_SUPPLY_PATH);
160 if !power_supply.exists() {
161 return false;
162 }
163
164 if let Ok(entries) = fs::read_dir(power_supply) {
165 for entry in entries.flatten() {
166 let path = entry.path();
167 let type_path = path.join("type");
168 if let Ok(type_content) = fs::read_to_string(&type_path) {
169 if type_content.trim() == "Mains" {
170 let online_path = path.join("online");
171 if let Ok(online) = fs::read_to_string(online_path) {
172 if online.trim() == "1" {
173 return true;
174 }
175 }
176 }
177 }
178 }
179 }
180 false
181}