redbear-power: upower client + render/sensor/battery/dmi updates

This commit is contained in:
2026-07-07 22:00:54 +03:00
parent 33c002d563
commit 596407a52d
7 changed files with 501 additions and 62 deletions
@@ -1,22 +1,23 @@
//! Battery information via `sysfs` (`/sys/class/power_supply/BAT0/*`).
//! Battery information.
//!
//! Linux hosts expose laptop batteries through the `power_supply` class.
//! On Redox, no equivalent scheme exists yet, so `read_battery()` returns
//! an `available=false` `BatteryInfo` and the render layer skips the
//! panel entirely — per the zero-stub policy.
//!
//! The Redox target needs a `power_supply` scheme daemon (likely wired to
//! ACPI); this is forward work tracked in the v1.6 docs.
//! On Linux the primary source is `/sys/class/power_supply/BAT0/*`. On
//! Redox, ACPI exposes battery and adapter state via the power scheme:
//! `/scheme/acpi/power/batteries/BAT0/{state,percentage}` and
//! `/scheme/acpi/power/adapters/AC/online`. If neither source is
//! available, `read_battery()` returns `available=false` and the render
//! layer skips the panel.
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const SYS_POWER_SUPPLY: &str = "/sys/class/power_supply";
const REDOX_POWER: &str = "/scheme/acpi/power";
#[derive(Default, Clone, Debug)]
pub struct BatteryInfo {
pub available: bool,
pub on_battery: Option<bool>,
pub name: Option<String>,
pub status: Option<String>,
pub capacity_percent: Option<u32>,
@@ -60,6 +61,37 @@ fn read_sysfs_f64_micro_to_units(path: &Path, unit_divisor: f64) -> Option<f64>
Some((raw as f64) / unit_divisor)
}
/// Read a Redox power scheme file.
fn read_redox(path: &Path) -> Option<String> {
read_sysfs(path)
}
fn read_redox_u32(path: &Path) -> Option<u32> {
read_redox(path)?.parse::<u32>().ok()
}
fn read_redox_f64(path: &Path) -> Option<f64> {
read_redox(path)?.parse::<f64>().ok()
}
/// Map a Redox battery state bitmask to a status string.
/// 0x1 = discharging, 0x2 = charging, 0x4 = empty
fn redox_state_to_status(state: u32) -> String {
if state == 0 {
return "Unknown".to_string();
}
if state & 0x4 != 0 {
return "Empty".to_string();
}
if state & 0x2 != 0 {
return "Charging".to_string();
}
if state & 0x1 != 0 {
return "Discharging".to_string();
}
"Unknown".to_string()
}
impl BatteryInfo {
/// Scan `/sys/class/power_supply/` for the first battery device
/// (`type == "Battery"`). Returns `None` if no battery is present
@@ -80,14 +112,74 @@ impl BatteryInfo {
None
}
/// Build a populated `BatteryInfo` from sysfs. Returns an empty
/// `available=false` struct if no battery is detected.
/// Find the first Redox battery directory under `/scheme/acpi/power/batteries/`.
fn find_redox_battery_dir() -> Option<PathBuf> {
let batteries_root = Path::new(REDOX_POWER).join("batteries");
let dir = fs::read_dir(&batteries_root).ok()?;
for entry in dir.flatten() {
if entry.path().is_dir() {
return Some(entry.path());
}
}
None
}
/// Read battery/AC status from the Redox ACPI power scheme.
fn read_redox() -> Self {
let mut info = Self {
available: true,
on_battery: None,
..Default::default()
};
let base = match Self::find_redox_battery_dir() {
Some(b) => b,
None => {
info.available = false;
return info;
}
};
info.name = base
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_string());
let state = read_redox_u32(&base.join("state")).unwrap_or(0);
info.status = Some(redox_state_to_status(state));
info.capacity_percent = read_redox_u32(&base.join("percentage"));
// Redox ACPI power scheme currently does not expose energy, power,
// voltage, time estimates, cycle count, or model metadata. Leave those
// as None so the render layer shows "?" for unsupported fields.
info
}
/// Build a populated `BatteryInfo` from the best available source.
/// Returns an empty `available=false` struct if no battery is detected.
pub fn read() -> Self {
let mut info = Self::read_native();
if !info.available {
// No local sysfs/scheme battery; try UPower D-Bus as a fallback.
if let Some(upower) = crate::upower_client::read_upower() {
return upower;
}
} else {
// We have a local battery; overlay UPower's authoritative
// status/percentage/on_battery values when possible.
info = crate::upower_client::merge_upower(info);
}
info
}
/// Build a `BatteryInfo` from local sysfs (Linux) or ACPI scheme
/// (Redox), without using UPower.
fn read_native() -> Self {
if Path::new(REDOX_POWER).exists() {
return Self::read_redox();
}
let Some(base) = Self::find_battery_dir() else {
return Self::default();
};
Self {
available: true,
on_battery: None,
name: read_sysfs(&base.join("name")),
status: read_sysfs(&base.join("status")),
capacity_percent: read_sysfs_u32(&base.join("capacity")),
@@ -1,20 +1,22 @@
//! SMBIOS / DMI motherboard information.
//!
//! Reads `/sys/class/dmi/id/*` on Linux hosts. On Redox, no equivalent
//! scheme exists yet, so `read_dmi()` returns an empty struct and the
//! render layer displays `?` for missing fields — per the zero-stub policy.
//!
//! The Redox target needs a `dmi` scheme daemon that exposes SMBIOS tables
//! via `/scheme/dmi/...`; this is forward work tracked in the v1.5 docs.
//! Reads `/sys/class/dmi/id/*` on Linux hosts. On Redox, the ACPI
//! scheme exposes SMBIOS data at `/scheme/acpi/dmi` as a single
//! `key=value` file (and optionally per-field files like
//! `/scheme/acpi/dmi/sys_vendor`). If neither source is available,
//! `read_dmi()` returns an empty struct and the render layer displays
//! `?` for missing fields.
use std::fs;
use std::path::Path;
/// Linux sysfs path for DMI/SMBIOS data.
const SYS_DMI: &str = "/sys/class/dmi/id";
/// Redox ACPI scheme path for DMI/SMBIOS data.
const REDOX_DMI: &str = "/scheme/acpi/dmi";
/// DMI/SMBIOS fields. All fields are `Option<String>` because any one of
/// them may be unreadable (permission denied, missing sysfs file, etc.).
/// them may be unreadable (permission denied, missing file, etc.).
#[derive(Default, Clone, Debug)]
pub struct DmiInfo {
pub board_vendor: Option<String>,
@@ -57,35 +59,93 @@ impl DmiInfo {
}
}
/// Probe whether `/sys/class/dmi/id/` exists. Used by the Sources
/// header line to report `dmi=ok` vs `dmi=no`.
pub fn available() -> bool {
Path::new(SYS_DMI).is_dir()
/// Read a single Redox per-field file (`/scheme/acpi/dmi/<field>`).
fn read_redox_field(name: &str) -> Option<String> {
let path = Path::new(REDOX_DMI).join(name);
match fs::read_to_string(&path) {
Ok(s) => {
let trimmed = s.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
Err(_) => None,
}
}
/// Build a populated `DmiInfo` from sysfs. Each field is read
/// independently so one failure doesn't poison the others.
/// Parse the Redox `/scheme/acpi/dmi` single-file format, which is a
/// set of `key=value` lines. Returns an empty map on any error.
fn parse_redox_dmi() -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
let Ok(content) = fs::read_to_string(REDOX_DMI) else {
return map;
};
for line in content.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
let trimmed = value.trim().to_string();
if !trimmed.is_empty() {
map.insert(key.trim().to_string(), trimmed);
}
}
map
}
/// Build a populated `DmiInfo` from Redox ACPI scheme data. Each field
/// is read independently from per-field files or the single-file
/// key=value fallback so one failure doesn't poison the others.
fn read_redox() -> Self {
let fields = Self::parse_redox_dmi();
let mut info = Self::default();
let fill = |key: &str, target: &mut Option<String>| {
*target = Self::read_redox_field(key).or_else(|| fields.get(key).cloned());
};
fill("sys_vendor", &mut info.sys_vendor);
fill("board_vendor", &mut info.board_vendor);
fill("board_name", &mut info.board_name);
fill("board_version", &mut info.board_version);
fill("product_name", &mut info.product_name);
fill("product_version", &mut info.product_version);
fill("bios_version", &mut info.bios_version);
// The remaining fields are not exposed by the Redox ACPI DMI scheme
// (per redox-driver-sys/src/quirks/dmi.rs); leave them as None.
info
}
/// Probe whether DMI data is available on this host.
pub fn available() -> bool {
Path::new(SYS_DMI).is_dir() || Path::new(REDOX_DMI).exists()
}
/// Build a populated `DmiInfo` from the best available source.
pub fn read() -> Self {
Self {
board_vendor: Self::read_sysfs("board_vendor"),
board_name: Self::read_sysfs("board_name"),
board_version: Self::read_sysfs("board_version"),
board_serial: Self::read_sysfs("board_serial"),
board_asset_tag: Self::read_sysfs("board_asset_tag"),
bios_vendor: Self::read_sysfs("bios_vendor"),
bios_version: Self::read_sysfs("bios_version"),
bios_date: Self::read_sysfs("bios_date"),
bios_release: Self::read_sysfs("bios_release"),
product_name: Self::read_sysfs("product_name"),
product_family: Self::read_sysfs("product_family"),
product_version: Self::read_sysfs("product_version"),
product_serial: Self::read_sysfs("product_serial"),
product_uuid: Self::read_sysfs("product_uuid"),
sys_vendor: Self::read_sysfs("sys_vendor"),
chassis_vendor: Self::read_sysfs("chassis_vendor"),
chassis_type: Self::read_sysfs("chassis_type"),
chassis_version: Self::read_sysfs("chassis_version"),
chassis_asset_tag: Self::read_sysfs("chassis_asset_tag"),
if Path::new(REDOX_DMI).exists() {
Self::read_redox()
} else {
Self {
board_vendor: Self::read_sysfs("board_vendor"),
board_name: Self::read_sysfs("board_name"),
board_version: Self::read_sysfs("board_version"),
board_serial: Self::read_sysfs("board_serial"),
board_asset_tag: Self::read_sysfs("board_asset_tag"),
bios_vendor: Self::read_sysfs("bios_vendor"),
bios_version: Self::read_sysfs("bios_version"),
bios_date: Self::read_sysfs("bios_date"),
bios_release: Self::read_sysfs("bios_release"),
product_name: Self::read_sysfs("product_name"),
product_family: Self::read_sysfs("product_family"),
product_version: Self::read_sysfs("product_version"),
product_serial: Self::read_sysfs("product_serial"),
product_uuid: Self::read_sysfs("product_uuid"),
sys_vendor: Self::read_sysfs("sys_vendor"),
chassis_vendor: Self::read_sysfs("chassis_vendor"),
chassis_type: Self::read_sysfs("chassis_type"),
chassis_version: Self::read_sysfs("chassis_version"),
chassis_asset_tag: Self::read_sysfs("chassis_asset_tag"),
}
}
}
@@ -74,6 +74,7 @@ mod session;
mod smart;
mod storage;
mod theme;
mod upower_client;
mod wakeup;
use crate::app::{App, POLL_MS, TabId};
@@ -977,7 +977,7 @@ pub fn render_battery_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
let bat = &app.battery;
if !bat.available {
return Paragraph::new(Line::from(
"(no battery detected — /sys/class/power_supply/BAT* not present)"
"(no battery detected — no UPower, /sys/class/power_supply, or /scheme/acpi/power battery found)"
.set_style(theme.value_warm),
))
.block(panel_border(focused, " Battery ", &app.theme))
@@ -1007,6 +1007,13 @@ pub fn render_battery_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
]));
lines.push(Line::from(""));
lines.push(Line::from("State".set_style(theme.label_bold)));
lines.push(Line::from(vec![
" On battery: ".set_style(theme.label),
bat.on_battery
.map(|b| if b { "yes" } else { "no" })
.unwrap_or("?")
.set_style(theme.value),
]));
lines.push(Line::from(vec![
" Status: ".set_style(theme.label),
crate::battery::BatteryInfo::display(&bat.status).set_style(theme.value),
@@ -20,6 +20,7 @@ use std::fs;
use std::path::{Path, PathBuf};
const SYS_HWMON: &str = "/sys/class/hwmon";
const REDOX_THERMAL: &str = "/scheme/thermal";
#[derive(Default, Clone, Debug)]
pub struct SensorReading {
@@ -123,6 +124,57 @@ fn read_first_cpu_temp_msr() -> Option<f64> {
None
}
/// Read a Redox thermal zone temperature file.
fn read_redox_zone_temp(zone_name: &str) -> Option<i64> {
let path = Path::new(REDOX_THERMAL)
.join("zones")
.join(zone_name)
.join("temperature");
read_sysfs_i64(&path)
}
/// Read a Redox thermal zone status file.
fn read_redox_zone_status(zone_name: &str) -> Option<String> {
let path = Path::new(REDOX_THERMAL)
.join("zones")
.join(zone_name)
.join("status");
read_sysfs(&path)
}
/// Enumerate Redox `/scheme/thermal/zones/` directories and return
/// one `HwmonChip`-like entry per zone with a temperature reading.
fn read_redox_thermal_zones() -> Vec<HwmonChip> {
let zones_root = Path::new(REDOX_THERMAL).join("zones");
let Ok(entries) = fs::read_dir(&zones_root) else {
return Vec::new();
};
let mut chips = Vec::new();
for entry in entries.flatten() {
let zone_name = match entry.file_name().into_string() {
Ok(n) => n,
Err(_) => continue,
};
let Some(temp_raw) = read_redox_zone_temp(&zone_name) else {
continue;
};
let label = read_redox_zone_status(&zone_name)
.map(|s| format!("{} ({})", zone_name, s))
.unwrap_or_else(|| zone_name.clone());
chips.push(HwmonChip {
name: "thermal".to_string(),
path: zones_root.join(&zone_name),
readings: vec![SensorReading {
kind: SensorKind::Temp,
label: Some(label),
raw_value: temp_raw,
display_value: format_sensor(SensorKind::Temp, temp_raw),
}],
});
}
chips
}
/// Read all `*_input` files in the chip directory, grouped by prefix.
fn read_chip_readings(chip_dir: &Path) -> Vec<SensorReading> {
let entries = match fs::read_dir(chip_dir) {
@@ -187,9 +239,17 @@ fn read_chip_readings(chip_dir: &Path) -> Vec<SensorReading> {
impl SensorInfo {
pub fn available() -> bool {
Path::new(SYS_HWMON).is_dir() || read_first_cpu_temp_msr().is_some()
Path::new(SYS_HWMON).is_dir()
|| Path::new(REDOX_THERMAL).exists()
|| read_first_cpu_temp_msr().is_some()
}
pub fn read() -> Self {
if Path::new(REDOX_THERMAL).exists() {
let chips = read_redox_thermal_zones();
if !chips.is_empty() {
return Self { chips };
}
}
let Ok(dirs) = fs::read_dir(SYS_HWMON) else {
if let Some(temp_c) = read_first_cpu_temp_msr() {
return Self {
@@ -1,25 +1,18 @@
//! Block device storage info via `sysfs` (`/sys/block/<dev>/`).
//! Block device storage info.
//!
//! Linux exposes block device metadata via sysfs: model, vendor, size
//! (in 512-byte sectors), rotational flag, removable flag, IO
//! scheduler, queue depth, and per-partition layout.
//!
//! Traffic counters come from `/sys/block/<dev>/stat`:
//! read_bytes / write_bytes — total bytes transferred
//! reads_completed / writes_completed — I/O operation counts
//!
//! SMART data (Temperature, ReallocatedSectorsCount, WearLevelingCount,
//! etc.) is read via `smartctl --json` if the binary is in PATH.
//! Otherwise the SMART section is omitted — per the zero-stub policy.
//!
//! On Redox, no equivalent scheme exists yet, so `read()` returns an
//! empty `StorageInfo` and the render layer shows
//! `(no storage devices detected)`.
//! On Linux the primary source is `/sys/block/<dev>/` (model, vendor,
//! size, rotational/removable flags, scheduler, queue depth, and
//! per-device `stat` counters). On Redox, the disk aggregator daemon
//! exposes block devices through `/scheme/diskd/`. Device metadata such
//! as model, vendor, and exact size are not currently available through
//! that scheme; only device names are enumerated, with I/O stats
//! remaining unavailable. Linux remains the fully-featured source.
use std::fs;
use std::path::{Path, PathBuf};
const SYS_BLOCK: &str = "/sys/block";
const REDOX_DISKD: &str = "/scheme/diskd";
#[derive(Default, Clone, Debug)]
pub struct DiskStats {
@@ -126,6 +119,25 @@ fn read_disk(name: &str, path: &Path) -> Option<DiskInfo> {
})
}
/// Read a Redox diskd entry. The disk aggregator scheme exposes block
/// device names but does not currently provide size, model, vendor, or I/O
/// counters; those fields are left as sensible defaults.
fn read_redox_disk(name: &str, path: &Path) -> DiskInfo {
DiskInfo {
name: name.to_string(),
path: path.to_path_buf(),
model: None,
vendor: None,
size_bytes: 0,
rotational: false,
removable: false,
scheduler: None,
queue_depth: None,
stats: DiskStats::default(),
partitions: Vec::new(),
}
}
impl DiskInfo {
/// Format bytes with binary unit suffixes (B, KiB, MiB, GiB, TiB).
pub fn format_size(bytes: u64) -> String {
@@ -160,9 +172,12 @@ pub struct StorageInfo {
impl StorageInfo {
pub fn available() -> bool {
Path::new(SYS_BLOCK).is_dir()
Path::new(SYS_BLOCK).is_dir() || Path::new(REDOX_DISKD).is_dir()
}
pub fn read() -> Self {
if Path::new(REDOX_DISKD).is_dir() {
return Self::read_redox();
}
let Ok(dirs) = fs::read_dir(SYS_BLOCK) else {
return Self::default();
};
@@ -179,6 +194,24 @@ impl StorageInfo {
disks.sort_by(|a, b| a.name.cmp(&b.name));
Self { disks }
}
/// Read diskd device names on Redox. The disk aggregator scheme
/// enumerates block devices but does not expose size/model/stats.
fn read_redox() -> Self {
let Ok(dirs) = fs::read_dir(REDOX_DISKD) else {
return Self::default();
};
let mut disks = Vec::new();
for entry in dirs.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
disks.push(read_redox_disk(name, &path));
}
disks.sort_by(|a, b| a.name.cmp(&b.name));
Self { disks }
}
/// Read disks and compute R/W throughput (KiB/s) for each based
/// on delta of read_bytes/write_bytes vs previous read.
pub fn read_with_throughput(prev: &StorageInfo, dt_secs: f64) -> Self {
@@ -0,0 +1,186 @@
//! D-Bus UPower client (system bus).
//!
//! Reads battery and AC adapter status from `org.freedesktop.UPower` so
//! redbear-power can display the same battery information on Linux
//! (real UPower) and Red Bear OS (redbear-upower compatibility daemon)
//! without relying on sysfs or ACPI scheme details.
//!
//! Implementation note: this module creates a short-lived tokio runtime
//! per call. Battery refresh is only every few seconds, so the overhead is
//! acceptable. A long-lived worker thread would be cleaner, but keeping it
//! synchronous simplifies the `BatteryInfo::read()` contract.
use tokio::runtime::Runtime;
use zbus::proxy;
use zbus::zvariant::OwnedObjectPath;
use crate::battery::BatteryInfo;
#[proxy(
interface = "org.freedesktop.UPower",
default_service = "org.freedesktop.UPower",
default_path = "/org/freedesktop/UPower"
)]
trait UPower {
/// Enumerate all power devices (batteries and AC adapters).
fn enumerate_devices(&self) -> zbus::Result<Vec<OwnedObjectPath>>;
/// Critical action the system would take on a critical battery event.
fn get_critical_action(&self) -> zbus::Result<String>;
/// True if the system is running on battery power.
#[zbus(property)]
fn on_battery(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn daemon_version(&self) -> zbus::Result<String>;
}
#[proxy(
interface = "org.freedesktop.UPower.Device",
default_service = "org.freedesktop.UPower"
)]
trait UPowerDevice {
/// 0=Unknown, 1=LinePower, 2=Battery.
#[zbus(property)]
fn type_(&self) -> zbus::Result<u32>;
/// 0=Unknown, 1=Charging, 2=Discharging, 3=Empty, 4=FullyCharged.
#[zbus(property)]
fn state(&self) -> zbus::Result<u32>;
/// Battery charge percentage (0.0100.0).
#[zbus(property)]
fn percentage(&self) -> zbus::Result<f64>;
/// Whether the device is physically present.
#[zbus(property)]
fn is_present(&self) -> zbus::Result<bool>;
/// For AC adapters: is the adapter online. For batteries: always false.
#[zbus(property)]
fn online(&self) -> zbus::Result<bool>;
/// Native (ACPI) path of the device.
#[zbus(property)]
fn native_path(&self) -> zbus::Result<String>;
}
/// Device type constants from the UPower specification.
mod device_type {
pub const UNKNOWN: u32 = 0;
pub const LINE_POWER: u32 = 1;
pub const BATTERY: u32 = 2;
}
/// Device state constants from the UPower specification.
mod device_state {
pub const UNKNOWN: u32 = 0;
pub const CHARGING: u32 = 1;
pub const DISCHARGING: u32 = 2;
pub const EMPTY: u32 = 3;
pub const FULLY_CHARGED: u32 = 4;
}
fn state_to_status(state: u32) -> String {
match state {
device_state::CHARGING => "Charging".to_string(),
device_state::DISCHARGING => "Discharging".to_string(),
device_state::EMPTY => "Empty".to_string(),
device_state::FULLY_CHARGED => "Full".to_string(),
_ => "Unknown".to_string(),
}
}
/// Read battery/AC status from the UPower D-Bus service. Returns `None` if
/// the system bus is unreachable or UPower is not available.
pub fn read_upower() -> Option<BatteryInfo> {
let rt = Runtime::new().ok()?;
rt.block_on(read_upower_async())
}
async fn read_upower_async() -> Option<BatteryInfo> {
let conn = zbus::connection::Builder::system()
.ok()?
.build()
.await
.ok()?;
let upower = UPowerProxy::new(&conn).await.ok()?;
let on_battery = upower.on_battery().await.unwrap_or(false);
let devices = upower.enumerate_devices().await.unwrap_or_default();
let mut info = BatteryInfo {
available: false,
..Default::default()
};
for path in devices {
let dev = match UPowerDeviceProxy::builder(&conn)
.path(path.clone())
.ok()?
.build()
.await
{
Ok(d) => d,
Err(_) => continue,
};
let dev_type = dev.type_().await.unwrap_or(device_type::UNKNOWN);
if dev_type == device_type::LINE_POWER {
// AC adapter: we expose its online state as the power_now_w
// field? No, we use a dedicated field if it existed. For now,
// represent the AC adapter as a synthetic device with name.
let _ = dev.online().await.unwrap_or(false);
// AC online state is not represented in BatteryInfo; it is
// implicit in the OnBattery daemon property. We leave the
// BatteryInfo focused on the battery device itself.
} else if dev_type == device_type::BATTERY {
let is_present = dev.is_present().await.unwrap_or(false);
if !is_present {
continue;
}
info.available = true;
let state = dev.state().await.unwrap_or(device_state::UNKNOWN);
info.status = Some(state_to_status(state));
info.capacity_percent = Some(dev.percentage().await.unwrap_or(0.0) as u32);
info.name = dev
.native_path()
.await
.ok()
.map(|p| p.rsplit('/').next().unwrap_or(&p).to_string());
info.on_battery = Some(on_battery);
// redbear-upower currently does not expose Energy, Power, Voltage,
// TimeToEmpty, TimeToFull, CycleCount, Technology, Model, Vendor,
// or Serial. Leave those fields as None so the render layer shows
// "?" for unsupported values.
}
}
if info.available { Some(info) } else { None }
}
/// Merge the UPower-derived fields into a base `BatteryInfo`. This is a
/// convenience helper so `battery.rs` can keep its Linux/Redox sysfs/scheme
/// fallback and only overlay the values that UPower provided.
pub fn merge_upower(mut base: BatteryInfo) -> BatteryInfo {
if let Some(upower) = read_upower() {
if base.available {
// UPower is authoritative for status and percentage; other fields
// that the local sysfs/scheme read already populated are kept.
if upower.status.is_some() {
base.status = upower.status;
}
if upower.capacity_percent.is_some() {
base.capacity_percent = upower.capacity_percent;
}
if upower.on_battery.is_some() {
base.on_battery = upower.on_battery;
}
base
} else {
upower
}
} else {
base
}
}