feat(power): subscribe to battery events

Adds subscriptions for watching battery status changes for the system and all
connected devices with batteries.

Closes #1317
This commit is contained in:
Michael Aaron Murphy 2025-09-24 09:57:13 +02:00 committed by Michael Murphy
parent 8f9ce2b30f
commit 47f6991708
2 changed files with 98 additions and 11 deletions

View file

@ -242,9 +242,10 @@ pub struct ConnectedDevice {
pub device_icon: &'static str,
pub battery: Battery,
pub device_path: String,
pub proxy: Option<DeviceProxy<'static>>,
}
async fn get_device_proxy<'a>() -> Result<upower_dbus::DeviceProxy<'a>, zbus::Error> {
pub async fn get_device_proxy<'a>() -> Result<upower_dbus::DeviceProxy<'a>, zbus::Error> {
let connection = match Connection::system().await {
Ok(c) => c,
Err(e) => {
@ -300,7 +301,7 @@ async fn enumerate_devices<'a>() -> Result<Vec<upower_dbus::DeviceProxy<'a>>, zb
}
impl Battery {
pub async fn from_device(proxy: DeviceProxy<'_>) -> Self {
pub async fn from_device(proxy: &DeviceProxy<'_>) -> Self {
let mut remaining_duration: Duration = Duration::default();
let (is_present, percentage, battery_state) = futures::join!(
@ -371,7 +372,7 @@ impl Battery {
let proxy = get_device_proxy().await;
if let Ok(proxy) = proxy {
return Self::from_device(proxy).await;
return Self::from_device(&proxy).await;
}
Battery::default()
@ -418,7 +419,7 @@ impl Battery {
}
impl ConnectedDevice {
async fn from_device_maybe(proxy: DeviceProxy<'_>) -> Option<Self> {
async fn from_device_maybe(proxy: DeviceProxy<'static>) -> Option<Self> {
let device_type = proxy.type_().await.unwrap_or(BatteryType::Unknown);
let device_path = proxy.clone().into_inner().path().to_string();
if matches!(
@ -431,7 +432,7 @@ impl ConnectedDevice {
.model()
.await
.unwrap_or(fl!("connected-devices", "unknown"));
let battery = Battery::from_device(proxy).await;
let battery = Battery::from_device(&proxy).await;
let device_icon = match device_type {
BatteryType::Ups => "uninterruptible-power-supply-symbolic",
BatteryType::Monitor => "display-symbolic",
@ -461,6 +462,7 @@ impl ConnectedDevice {
device_icon,
battery,
device_path,
proxy: Some(proxy),
})
}

View file

@ -4,20 +4,21 @@ use self::backend::{GetCurrentPowerProfile, SetPowerProfile};
use backend::{Battery, ConnectedDevice, PowerProfile};
use chrono::TimeDelta;
use cosmic::Task;
use cosmic::iced::{Alignment, Length};
use cosmic::iced::{self, Alignment, Length};
use cosmic::iced_widget::{column, row};
use cosmic::widget::{self, radio, settings, text};
use cosmic::{Apply, surface};
use cosmic::{Task, iced_futures};
use cosmic_config::{Config, CosmicConfigEntry};
use cosmic_idle_config::CosmicIdleConfig;
use cosmic_settings_page::{self as page, Section, section};
use futures::StreamExt;
use futures::{SinkExt, StreamExt};
use itertools::Itertools;
use slab::Slab;
use slotmap::SlotMap;
use std::iter;
use std::time::Duration;
use upower_dbus::DeviceProxy;
static SCREEN_OFF_TIMES: &[Duration] = &[
Duration::from_secs(2 * 60),
@ -115,6 +116,66 @@ impl page::Page<crate::pages::Message> for Page {
])
}
fn subscription(
&self,
_core: &cosmic::Core,
) -> cosmic::iced::Subscription<crate::pages::Message> {
// Shared logic between the system battery and connected device batteries.
async fn receive_battery_changes(
proxy: DeviceProxy<'static>,
device_path: String,
mut sender: futures::channel::mpsc::Sender<crate::pages::Message>,
message_fn: fn(String, Battery) -> Message,
) {
let mut battery_level = proxy.receive_battery_level_changed().await;
let mut battery_state = proxy.receive_state_changed().await;
loop {
let _ = futures::future::select(battery_level.next(), battery_state.next()).await;
_ = sender
.send(
message_fn(device_path.clone(), Battery::from_device(&proxy).await).into(),
)
.await;
}
}
// A subscription for the system battery.
let system_battery = iced::Subscription::run(|| {
iced_futures::stream::channel(1, |sender| async move {
if let Ok(proxy) = backend::get_device_proxy().await {
receive_battery_changes(proxy, String::new(), sender, |_, b| {
Message::UpdateBattery(b)
})
.await;
}
})
});
// Subscriptions for all connected device batteries.
let device_batteries = self
.connected_devices
.iter()
.filter_map(|device| {
device
.proxy
.clone()
.map(|p| (device.device_path.clone(), p))
})
.map(|(path, proxy)| {
iced::Subscription::run_with_id(
path.clone(),
iced_futures::stream::channel(1, |sender| async move {
receive_battery_changes(proxy, path, sender, Message::UpdateDeviceBattery)
.await
}),
)
});
iced::Subscription::batch(std::iter::once(system_battery).chain(device_batteries))
}
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
let futures: Vec<Task<Message>> = vec![
cosmic::Task::future(async move {
@ -139,7 +200,7 @@ impl page::Page<crate::pages::Message> for Page {
}
}),
cosmic::Task::run(
async_fn_stream::fn_stream(|emitter| async move {
iced_futures::stream::channel(1, |mut emitter| async move {
let span = tracing::span!(tracing::Level::INFO, "power::device_stream task");
let _span_handle = span.enter();
@ -151,13 +212,14 @@ impl page::Page<crate::pages::Message> for Page {
let added_stream = ConnectedDevice::device_added_stream(&connection).await;
let removed_stream = ConnectedDevice::device_removed_stream(&connection).await;
let mut sender = emitter.clone();
let added_future = std::pin::pin!(async {
match added_stream {
Ok(stream) => {
let mut stream = std::pin::pin!(stream);
while let Some(device) = stream.next().await {
tracing::debug!(device = device.model, "device added");
emitter.emit(Message::DeviceConnect(device)).await;
_ = sender.send(Message::DeviceConnect(device)).await;
}
}
Err(err) => tracing::error!(?err, "cannot establish added stream"),
@ -170,7 +232,7 @@ impl page::Page<crate::pages::Message> for Page {
let mut stream = std::pin::pin!(stream);
while let Some(device_path) = stream.next().await {
tracing::debug!(device_path, "device removed");
emitter.emit(Message::DeviceDisconnect(device_path)).await;
_ = emitter.send(Message::DeviceDisconnect(device_path)).await;
}
}
Err(err) => tracing::error!(?err, "cannot establish removed stream"),
@ -203,7 +265,10 @@ impl page::Page<crate::pages::Message> for Page {
#[derive(Clone, Debug)]
pub enum Message {
PowerProfileChange(PowerProfile),
/// Update the system battery
UpdateBattery(Battery),
/// Update the battery of a connected device
UpdateDeviceBattery(String, Battery),
UpdateConnectedDevices(Vec<ConnectedDevice>),
DeviceDisconnect(String),
DeviceConnect(ConnectedDevice),
@ -215,6 +280,18 @@ pub enum Message {
Surface(surface::Action),
}
impl From<Message> for crate::app::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Power(message).into()
}
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Power(message)
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
match message {
@ -229,6 +306,14 @@ impl Page {
}
}
Message::UpdateBattery(battery) => self.battery = battery,
Message::UpdateDeviceBattery(path, battery) => {
for device in &mut self.connected_devices {
if device.device_path == path {
device.battery = battery;
break;
}
}
}
Message::UpdateConnectedDevices(connected_devices) => {
self.connected_devices = connected_devices;
}