use futures_util::stream::{StreamExt, select_all};
use log::warn;
use zbus::{Connection, proxy};
use crate::widget::{Battery, BatteryState, Msg};
use crate::producer::{MsgSender, Producer, ProducerFuture, ProducerResult};
pub fn upower_state(raw: u32) -> BatteryState {
match raw {
1 | 5 => BatteryState::Charging,
2 | 3 | 6 => BatteryState::Discharging,
4 => BatteryState::Full,
_ => BatteryState::Unknown,
}
}
pub fn battery_from_upower(raw_state: u32, percentage: f64, is_present: bool) -> Option<Battery> {
if !is_present {
return None;
}
Some(Battery::new(upower_state(raw_state), percentage))
}
pub fn time_remaining(state: BatteryState, to_empty: i64, to_full: i64) -> i64 {
match state {
BatteryState::Discharging => to_empty,
BatteryState::Charging => to_full,
BatteryState::Full | BatteryState::Unknown => 0,
}
}
#[proxy(
interface = "org.freedesktop.UPower.Device",
default_service = "org.freedesktop.UPower",
default_path = "/org/freedesktop/UPower/devices/DisplayDevice"
)]
trait UPowerDevice {
#[zbus(property)]
fn percentage(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn state(&self) -> zbus::Result<u32>;
#[zbus(property)]
fn is_present(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn time_to_empty(&self) -> zbus::Result<i64>;
#[zbus(property)]
fn time_to_full(&self) -> zbus::Result<i64>;
}
#[hotpath::measure]
async fn read_snapshot(device: &UPowerDeviceProxy<'_>) -> Option<Battery> {
match try_read(device).await {
Ok(snapshot) => snapshot,
Err(e) => {
warn!("upower: reading battery state failed: {e}");
None
}
}
}
#[hotpath::measure]
async fn try_read(device: &UPowerDeviceProxy<'_>) -> zbus::Result<Option<Battery>> {
let is_present = device.is_present().await?;
let percentage = device.percentage().await?;
let state = device.state().await?;
let Some(battery) = battery_from_upower(state, percentage, is_present) else {
return Ok(None);
};
let to_empty = device.time_to_empty().await.unwrap_or(0);
let to_full = device.time_to_full().await.unwrap_or(0);
Ok(Some(battery.with_time(time_remaining(
battery.state(),
to_empty,
to_full,
))))
}
pub struct UPowerProducer;
impl UPowerProducer {
pub fn new() -> Self {
Self
}
}
impl Default for UPowerProducer {
fn default() -> Self {
Self::new()
}
}
impl Producer for UPowerProducer {
fn name(&self) -> String {
"upower-battery".to_string()
}
fn run(self: Box<Self>, tx: MsgSender) -> ProducerFuture {
Box::pin(run(tx))
}
}
async fn run(tx: MsgSender) -> ProducerResult {
let conn = Connection::system().await?;
let device = UPowerDeviceProxy::new(&conn).await?;
if tx.send(Msg::Battery(read_snapshot(&device).await)).is_err() {
return Ok(());
}
let mut changes = select_all([
device
.receive_percentage_changed()
.await
.map(|_| ())
.boxed(),
device.receive_state_changed().await.map(|_| ()).boxed(),
device
.receive_is_present_changed()
.await
.map(|_| ())
.boxed(),
device
.receive_time_to_empty_changed()
.await
.map(|_| ())
.boxed(),
device
.receive_time_to_full_changed()
.await
.map(|_| ())
.boxed(),
]);
while changes.next().await.is_some() {
if tx.send(Msg::Battery(read_snapshot(&device).await)).is_err() {
return Ok(());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn charging_states_normalize_to_charging() {
assert_eq!(upower_state(1), BatteryState::Charging);
assert_eq!(upower_state(5), BatteryState::Charging);
}
#[test]
fn on_battery_states_normalize_to_discharging() {
assert_eq!(upower_state(2), BatteryState::Discharging);
assert_eq!(upower_state(3), BatteryState::Discharging);
assert_eq!(upower_state(6), BatteryState::Discharging);
}
#[test]
fn fully_charged_normalizes_to_full() {
assert_eq!(upower_state(4), BatteryState::Full);
}
#[test]
fn unknown_and_unexpected_states_normalize_to_unknown() {
assert_eq!(upower_state(0), BatteryState::Unknown);
assert_eq!(upower_state(99), BatteryState::Unknown);
}
#[test]
fn present_battery_normalizes_into_a_snapshot() {
let battery = battery_from_upower(2, 84.6, true).expect("present");
assert_eq!(battery.state(), BatteryState::Discharging);
assert_eq!(battery.percent(), 85);
assert_eq!(battery.label(), "85% discharging");
}
#[test]
fn time_remaining_follows_the_charge_direction() {
assert_eq!(time_remaining(BatteryState::Discharging, 600, 1200), 600);
assert_eq!(time_remaining(BatteryState::Charging, 600, 1200), 1200);
assert_eq!(time_remaining(BatteryState::Full, 600, 1200), 0);
assert_eq!(time_remaining(BatteryState::Unknown, 600, 1200), 0);
}
#[test]
fn absent_battery_normalizes_to_none() {
assert_eq!(battery_from_upower(2, 0.0, false), None);
}
}