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))
}
#[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>;
}
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
}
}
}
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?;
Ok(battery_from_upower(state, percentage, is_present))
}
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(),
]);
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 absent_battery_normalizes_to_none() {
assert_eq!(battery_from_upower(2, 0.0, false), None);
}
}