use crate::{
bat::{health::Health, soc::SoC},
error::PwrError,
read_number,
units::Unit,
Type,
};
use log::{debug, warn};
use std::{
default::Default,
fmt::Debug,
path::{Path, PathBuf},
};
use uom::si::{electric_current::microampere, electric_potential::microvolt, f32::*};
pub struct Battery {
path: PathBuf,
}
#[derive(Debug, Default)]
pub struct BatteryStatsRaw {
capacity: Option<u32>,
capacity_level: Option<String>,
charge_full: Option<u32>,
charge_full_design: Option<u32>,
charge_now: Option<u32>,
current_now: Option<u32>,
cycle_count: Option<u32>,
manufacturer: Option<String>,
name: Option<String>,
model_name: Option<String>,
present: Option<bool>,
serial_number: Option<String>,
status: Option<String>,
technology: Option<String>,
voltage_min_design: Option<u32>,
voltage_now: Option<u32>,
}
#[allow(dead_code)]
pub struct BatteryStats {
raw_stats: BatteryStatsRaw,
voltage: Option<ElectricPotential>,
current: Option<ElectricCurrent>,
power: Option<Power>,
soc: Option<SoC>,
health: Option<Health>,
}
impl BatteryStatsRaw {
pub fn new(bat: &Battery) -> Result<BatteryStatsRaw, PwrError> {
let mut stats = BatteryStatsRaw::default();
let uevent =
std::fs::read_to_string(bat.path.join("uevent")).map_err(PwrError::GenericError)?;
for line in uevent.lines() {
if let Some((key, value)) = line.split_once('=') {
let key = key.strip_prefix("POWER_SUPPLY_").unwrap();
let value = value.trim_end();
match key {
"NAME" => stats.name = value.parse().ok(),
"STATUS" => stats.status = value.parse().ok(),
"PRESENT" => stats.present = value.parse().ok(),
"TECHNOLOGY" => stats.technology = value.parse().ok(),
"CYCLE_COUNT" => stats.cycle_count = value.parse().ok(),
"VOLTAGE_MIN_DESIGN" => stats.voltage_min_design = value.parse().ok(),
"VOLTAGE_NOW" => stats.voltage_now = value.parse().ok(),
"CURRENT_NOW" => stats.current_now = value.parse().ok(),
"CHARGE_FULL_DESIGN" => stats.charge_full_design = value.parse().ok(),
"CHARGE_FULL" => stats.charge_full = value.parse().ok(),
"CHARGE_NOW" => stats.charge_now = value.parse().ok(),
"CAPACITY" => stats.capacity = value.parse().ok(),
"CAPACITY_LEVEL" => stats.capacity_level = value.parse().ok(),
"MODEL_NAME" => stats.model_name = value.parse().ok(),
"MANUFACTURER" => stats.manufacturer = value.parse().ok(),
"SERIAL_NUMBER" => stats.serial_number = value.parse().ok(),
"TYPE" => (),
_ => warn!(
"Found unknown battery property '{}' with value '{}'",
key, value
),
};
} else {
debug!("Don't know how to treat line with content '{}'", line);
};
}
Ok(stats)
}
}
impl BatteryStats {
pub fn new(stats: BatteryStatsRaw) -> Result<BatteryStats, PwrError> {
let v = stats
.voltage_now
.map(|val| ElectricPotential::new::<microvolt>(val as f32));
let c = stats
.current_now
.map(|val| ElectricCurrent::new::<microampere>(val as f32));
let p = match (v, c) {
(Some(v), Some(c)) => Some(v * c),
_ => None,
};
let soc = match (stats.charge_now, stats.charge_full) {
(Some(c_n), Some(c_f)) => Some(SoC::new(c_n as f32 / c_f as f32)?),
_ => stats
.capacity
.and_then(|val| SoC::new(val as f32 / 100.0).ok()),
};
let health = match (stats.charge_full, stats.charge_full_design) {
(Some(c_f), Some(c_f_d)) => Some(Health::new(c_f as f32 / c_f_d as f32)?),
_ => None,
};
Ok(BatteryStats {
raw_stats: stats,
voltage: v,
current: c,
power: p,
soc,
health,
})
}
}
impl Battery {
pub fn new(path: &Path) -> Result<Battery, PwrError> {
let path = PathBuf::from(path);
if !path.exists() {
return Err(PwrError::NoSysfs(path));
}
Ok(Battery { path })
}
pub fn stats_raw(&self) -> Result<BatteryStatsRaw, PwrError> {
BatteryStatsRaw::new(self)
}
pub fn stats(&self) -> Result<BatteryStats, PwrError> {
BatteryStats::new(self.stats_raw()?)
}
pub fn voltage(&self) -> Result<f32, PwrError> {
let voltage = read_number(self.path.join("voltage_now"))?;
debug!("Voltage is {:?} uV", voltage);
Ok(voltage as f32 / 1.0e6)
}
pub fn capacity(&self) -> Result<SoC, PwrError> {
let charge_cur = read_number(self.path.join("charge_now"))?;
let charge_full = read_number(self.path.join("charge_full"))?;
SoC::new(charge_cur as f32 / charge_full as f32)
}
pub fn status(&self) -> Result<String, PwrError> {
Err(PwrError::NotImplemented("status".into()))
}
pub fn current(&self) -> Result<f32, PwrError> {
let current = read_number(self.path.join("current_now"))?;
debug!("Current is {:?} uA", current);
Ok(current as f32 / 1.0e6)
}
pub fn health(&self) -> Result<Health, PwrError> {
let charge_full = read_number(self.path.join("charge_full"))?;
let charge_full_design = read_number(self.path.join("charge_full_design"))?;
Health::new(charge_full as f32 / charge_full_design as f32)
}
pub fn default() -> Result<Self, PwrError> {
let sysfs = PathBuf::from("/sys/class/power_supply");
for entry in std::fs::read_dir(&sysfs)? {
let entry = entry?;
let entry_name = entry.file_name().to_str().unwrap().to_string();
debug!("Checking sysfs entry {}", entry.path().display());
if !std::fs::metadata(entry.path())?.file_type().is_dir() {
debug!(" Skipping non-directory {}", entry_name);
continue;
}
let pwr_type: Type = std::fs::read_to_string(entry.path().join("type"))?.parse()?;
if pwr_type == Type::Battery {
debug!(" Is battery");
return Battery::new(&entry.path());
} else {
debug!(" Skipping entry of type {:?}", pwr_type);
}
}
Err(PwrError::NoBattery(sysfs))
}
}
impl std::fmt::Display for Battery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let stats = self.stats().unwrap();
let value_unknown = String::from("?");
let v = match stats.voltage {
Some(value) => format!("{}", Unit::V(value)),
None => value_unknown.clone(),
};
let a = match stats.current {
Some(value) => format!("{}", Unit::A(value)),
None => value_unknown.clone(),
};
let w = match stats.power {
Some(value) => format!("{}", Unit::W(value)),
None => value_unknown.clone(),
};
let soc = match stats.soc {
Some(value) => format!("{}", value),
None => value_unknown.clone(),
};
let health_text = match stats.health.as_ref() {
Some(value) => value.text(),
None => value_unknown.clone(),
};
let health = match stats.health {
Some(value) => format!("{}", value),
None => value_unknown,
};
write!(
f,
"Battery: {}\n\n- Voltage : {}\n- Current : {}\n- Power : {}\n- Capacity: {}\n- Health : {} ({})\n",
self.path.display(), v, a, w, soc, health_text, health
)
}
}
#[cfg(test)]
mod tests {
use crate::bat::Battery;
use crate::tests::dirs;
#[test]
fn create() {
let bat = Battery::new(&dirs::sysfs().join("BAT0"));
assert!(bat.is_ok());
}
#[test]
fn create_nonexistent_path() {
let bat = Battery::new(&dirs::sysfs().join("yxz"));
assert!(bat.is_err());
}
#[test]
fn battery_voltage() {
let bat = Battery::new(&dirs::sysfs().join("BAT0")).unwrap();
let voltage = bat.voltage().unwrap();
assert!(voltage > 0.0);
}
#[test]
fn current() {
let bat = Battery::new(&dirs::sysfs().join("BAT0")).unwrap();
let current = bat.current().unwrap();
assert!(current > 0.0);
}
}