pwr 0.2.2

Check power supplies of a system
Documentation
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::*};

/// A representation for a battery.
///
/// All battery-related values (such as current charge, voltage, current, etc.) are read via
/// methods. This is to ensure that they are read from sysfs on-demand and always contain
/// up-to-date values.
///
/// If you want to read multiple values at once, refer to the [`stats`] and [`stats_raw`]
/// functions.
pub struct Battery {
    path: PathBuf,
}

/// Raw battery stats, read directly from sysfs.
///
/// These are parsed from the `uevent` file in the batteries sysfs location.
#[derive(Debug, Default)]
pub struct BatteryStatsRaw {
    // Whether a battery alarm was triggered
    // TODO: What does this mean??
    //alarm: Option<bool>,
    /// State of charge
    capacity: Option<u32>,
    capacity_level: Option<String>,
    charge_full: Option<u32>,
    charge_full_design: Option<u32>,
    charge_now: Option<u32>,
    /// Current flow from/to battery (depends on status)
    current_now: Option<u32>,
    /// Number of charge cycles
    cycle_count: Option<u32>,
    /// Battery manufacturer
    manufacturer: Option<String>,
    /// Battery name in sysfs
    name: Option<String>,
    /// Model name
    model_name: Option<String>,
    /// 1 if battery is present, 0 otherwise
    present: Option<bool>,
    /// Serial number
    serial_number: Option<String>,
    /// Status (charging/discharging)
    status: Option<String>,
    /// Cell technology (e.g. Li-Ion)
    technology: Option<String>,
    /// Minimal battery voltage as designed
    voltage_min_design: Option<u32>,
    /// Battery voltage
    voltage_now: Option<u32>,
}

/// Processed battery stats.
///
/// These are calculated from the [`RawBatteryStats`] and contain additional info not directly
/// contained in sysfs.
///
/// Note that `voltage`, `current` and `power` use the `uom` crate. To obtain these in a specific
/// unit of measurement, refer to [`get` from the `uom` crate][1]
///
/// [1]: https://docs.rs/uom/latest/uom/si/struct.Quantity.html#method.get
#[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 {
    /// Parse battery stats from sysfs.
    ///
    /// Reads in the contents of the `uevent` file to determine the batteries current stats.
    ///
    /// # TODO
    ///
    /// Maybe this can be done with serde as well?
    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();
                // Remove trailing newlines
                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 {
    /// Process raw stats to some more useful information.
    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,
        };
        // charge_now / charge_full
        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()),
        };
        // charge_full / charge_full_design
        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 {
    /// Create a new battery instance.
    ///
    /// Expects a path to a battery sysfs entry, such as `/sys/class/power_supply/BAT0`, or
    /// something similar. Note that there isn't a strict naming convention regarding sysfs entries
    /// for batteries, so yours may be named differently.
    pub fn new(path: &Path) -> Result<Battery, PwrError> {
        let path = PathBuf::from(path);
        if !path.exists() {
            return Err(PwrError::NoSysfs(path));
        }

        Ok(Battery { path })
    }

    /// Read raw battery stats from sysfs.
    ///
    /// Contents are parsed from the `uevent` file to make sure they are all sampled at the same
    /// time and to minimize number of file operations.
    ///
    /// If you only need few battery stats (e.g. only Voltage and SoC), you may prefer to use the
    /// associated [`voltage`] and [`capacity`] functions of the Battery struct instead. If you
    /// want all the battery stats, use this function.
    pub fn stats_raw(&self) -> Result<BatteryStatsRaw, PwrError> {
        BatteryStatsRaw::new(self)
    }

    pub fn stats(&self) -> Result<BatteryStats, PwrError> {
        BatteryStats::new(self.stats_raw()?)
    }

    /// Return a batteries voltage in units of V.
    ///
    /// Can return a [`PwrError`] if e.g. the required sysfs entries don't exist or aren't
    /// accessible.
    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)
    }

    /// Returns the batteries state of charge.
    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)
    }

    /// Returns the batteries status.
    pub fn status(&self) -> Result<String, PwrError> {
        // TODO: Implement BatteryStatus enum
        Err(PwrError::NotImplemented("status".into()))
    }

    /// Returns the batteries current in units of A.
    ///
    /// To determine whether the battery is being charged or discharged, refer to [`status`].
    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)
    }

    /// Get a default battery.
    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);
    }
}