jolt_platform/linux/
battery.rs

1use 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}