feat(power): add connected devices section

This commit is contained in:
Antoine Colombier 2024-09-05 14:59:43 +01:00 committed by GitHub
parent 97bcbc64ec
commit 40d56e6ea7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 293 additions and 75 deletions

2
Cargo.lock generated
View file

@ -6934,7 +6934,7 @@ checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]] [[package]]
name = "upower_dbus" name = "upower_dbus"
version = "0.3.2" version = "0.3.2"
source = "git+https://github.com/pop-os/dbus-settings-bindings#7aedc25e3295b95a90eb710f443029d4ec920aa8" source = "git+https://github.com/pop-os/dbus-settings-bindings#e0d6a04d6ebf6bcede1580721c84a7f01e5ef8bb"
dependencies = [ dependencies = [
"serde", "serde",
"serde_repr", "serde_repr",

View file

@ -1,5 +1,7 @@
use chrono::{Duration, TimeDelta}; use chrono::{Duration, TimeDelta};
use futures::future::join_all;
use futures::FutureExt; use futures::FutureExt;
use upower_dbus::{BatteryType, DeviceProxy};
use zbus::Connection; use zbus::Connection;
mod ppdaemon; mod ppdaemon;
@ -238,6 +240,13 @@ pub struct Battery {
pub remaining_duration: Duration, pub remaining_duration: Duration,
} }
#[derive(Default, Debug, Clone)]
pub struct ConnectedDevice {
pub model: String,
pub device_icon: &'static str,
pub battery: Battery,
}
async fn get_device_proxy<'a>() -> Result<upower_dbus::DeviceProxy<'a>, zbus::Error> { async fn get_device_proxy<'a>() -> Result<upower_dbus::DeviceProxy<'a>, zbus::Error> {
let connection = match Connection::system().await { let connection = match Connection::system().await {
Ok(c) => c, Ok(c) => c,
@ -253,6 +262,44 @@ async fn get_device_proxy<'a>() -> Result<upower_dbus::DeviceProxy<'a>, zbus::Er
} }
} }
async fn enumerate_devices<'a>() -> Result<Vec<upower_dbus::DeviceProxy<'a>>, zbus::Error> {
let connection = match Connection::system().await {
Ok(c) => c,
Err(e) => {
tracing::error!("zbus connection failed. {e}");
return Err(e);
}
};
let devices = upower_dbus::UPowerProxy::new(&connection)
.await?
.enumerate_devices()
.await?;
let devices = futures::future::join_all(
devices
.into_iter()
.map(|path| DeviceProxy::new(&connection, path)),
)
.await;
let errors = devices.iter().filter(|device| device.is_err());
if errors.count() > 0 {
let mut errors: Vec<zbus::Error> = devices
.into_iter()
.filter_map(std::result::Result::err)
.collect();
if errors.len() > 1 {
eprintln!("Multiple errors occurs when fetching connected device: {errors:?}. Only the last one will be returned.");
}
return Err(errors.pop().unwrap());
}
Ok(devices
.into_iter()
.filter_map(std::result::Result::ok)
.collect())
}
async fn get_on_battery_status() -> Result<bool, zbus::Error> { async fn get_on_battery_status() -> Result<bool, zbus::Error> {
let connection = match Connection::system().await { let connection = match Connection::system().await {
Ok(c) => c, Ok(c) => c,
@ -269,71 +316,74 @@ async fn get_on_battery_status() -> Result<bool, zbus::Error> {
} }
impl Battery { impl Battery {
pub async fn update_battery() -> Self { pub async fn from_device(proxy: DeviceProxy<'_>) -> Self {
let proxy = get_device_proxy().await; let mut remaining_duration: Duration = Duration::default();
if let Ok(proxy) = proxy { let (is_present, percentage, on_battery) = futures::join!(
let mut remaining_duration: Duration = Duration::default(); proxy.is_present().map(Result::unwrap_or_default),
proxy.percentage().map(Result::unwrap_or_default),
get_on_battery_status().map(Result::unwrap_or_default)
);
let (is_present, percentage, on_battery) = futures::join!( let percent = percentage.clamp(0.0, 100.0);
proxy.is_present().map(Result::unwrap_or_default),
proxy.percentage().map(Result::unwrap_or_default),
get_on_battery_status().map(Result::unwrap_or_default)
);
let percent = percentage.clamp(0.0, 100.0); if on_battery {
if let Ok(time) = proxy.time_to_empty().await {
if on_battery {
if let Ok(time) = proxy.time_to_empty().await {
if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64))
{
remaining_duration = dur;
}
}
} else if let Ok(time) = proxy.time_to_full().await {
if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64)) { if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64)) {
remaining_duration = dur; remaining_duration = dur;
} }
} }
} else if let Ok(time) = proxy.time_to_full().await {
if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64)) {
remaining_duration = dur;
}
}
let battery_percent = if percent > 95.0 { let battery_percent = if percent > 95.0 {
100 100
} else if percent > 80.0 { } else if percent > 80.0 {
90 90
} else if percent > 65.0 { } else if percent > 65.0 {
80 80
} else if percent > 35.0 { } else if percent > 35.0 {
50 50
} else if percent > 20.0 { } else if percent > 20.0 {
35 35
} else if percent > 14.0 { } else if percent > 14.0 {
20 20
} else if percent > 9.0 { } else if percent > 9.0 {
10 10
} else if percent > 5.0 { } else if percent > 5.0 {
5 5
} else { } else {
0 0
}; };
let charging = if on_battery { "" } else { "charging-" }; let charging = if on_battery { "" } else { "charging-" };
let icon_name = let icon_name =
format!("cosmic-applet-battery-level-{battery_percent}-{charging}symbolic",); format!("cosmic-applet-battery-level-{battery_percent}-{charging}symbolic",);
return Battery { Self {
icon_name, icon_name,
is_present, is_present,
percent, percent,
on_battery, on_battery,
remaining_duration, remaining_duration,
}; }
}
pub async fn update_battery() -> Self {
let proxy = get_device_proxy().await;
if let Ok(proxy) = proxy {
return Self::from_device(proxy).await;
} }
Battery::default() Battery::default()
} }
pub fn remaining_time(&self) -> String { pub fn remaining_time(&self) -> String {
if self.remaining_duration <= TimeDelta::zero() { if self.remaining_duration <= TimeDelta::zero() {
return String::new() return String::new();
} }
let total_seconds = self.remaining_duration.num_seconds(); let total_seconds = self.remaining_duration.num_seconds();
@ -357,8 +407,11 @@ impl Battery {
let last = time.pop().unwrap(); let last = time.pop().unwrap();
time = vec![time.join(", "), last]; time = vec![time.join(", "), last];
} }
let time = if time.is_empty() { fl!("battery", "less-than-minute") let time = if time.is_empty() {
} else {time.join(&format!(" {} ", fl!("battery", "and"))) }; fl!("battery", "less-than-minute")
} else {
time.join(&format!(" {} ", fl!("battery", "and")))
};
fl!( fl!(
"battery", "battery",
@ -396,3 +449,67 @@ mod tests {
} }
} }
} }
impl ConnectedDevice {
async fn from_device_maybe(proxy: DeviceProxy<'_>) -> Option<Self> {
let device_type = proxy.type_().await.unwrap_or(BatteryType::Unknown);
if matches!(
device_type,
BatteryType::Unknown | BatteryType::LinePower | BatteryType::Battery
) {
return None;
}
let model = proxy
.model()
.await
.unwrap_or(fl!("connected-devices", "unknown"));
let battery = Battery::from_device(proxy).await;
let device_icon = match device_type {
BatteryType::Ups => "uninterruptible-power-supply-symbolic",
BatteryType::Monitor => "display-symbolic",
BatteryType::Mouse => "input-mouse-symbolic",
BatteryType::Keyboard => "input-keyboard-symbolic",
BatteryType::Pda | BatteryType::Phone => "smartphone-symbolic",
BatteryType::MediaPlayer => "multimedia-player-symbolic",
BatteryType::Tablet => "tablet-symbolic",
BatteryType::Computer => "laptop-symbolic",
BatteryType::GamingInput => "input-gaming-symbolic",
BatteryType::Pen => "input-tablet-symbolic",
BatteryType::Touchpad => "input-touchpad-symbolic",
BatteryType::Network => "network-wired-symbolic",
BatteryType::Headset => "audio-headset-symbolic",
BatteryType::Speakers => "speaker-symbolic",
BatteryType::Headphones => "audio-headphones-symbolic",
BatteryType::Video => "video-display-symbolic",
BatteryType::OtherAudio => "audio-speakers-symbolic",
BatteryType::Printer => "printer-network-symbolic",
BatteryType::Scanner => "scanner-symbolic",
BatteryType::Camera => "camera-photo-symbolic",
_ => "bluetooth-symbolic",
};
Some(Self {
model,
device_icon,
battery,
})
}
pub async fn update_connected_devices() -> Vec<Self> {
let proxy = enumerate_devices().await;
if let Ok(devices) = proxy {
return join_all(
devices
.into_iter()
.map(|device| Self::from_device_maybe(device)),
)
.await
.into_iter()
.flatten()
.collect();
}
vec![]
}
}

View file

@ -1,19 +1,24 @@
mod backend; mod backend;
use self::backend::{GetCurrentPowerProfile, SetPowerProfile}; use self::backend::{GetCurrentPowerProfile, SetPowerProfile};
use backend::{Battery, PowerProfile}; use backend::{Battery, ConnectedDevice, PowerProfile};
use chrono::TimeDelta;
use cosmic::iced::{Alignment, Length}; use cosmic::iced::{Alignment, Length};
use cosmic::iced_widget::row; use cosmic::iced_widget::{column, row};
use cosmic::widget::{self, column, radio, settings, text}; use cosmic::prelude::CollectionWidget;
use cosmic::widget::{self, radio, settings, text};
use cosmic::Apply; use cosmic::Apply;
use cosmic::Command;
use cosmic_settings_page::{self as page, section, Section}; use cosmic_settings_page::{self as page, section, Section};
use itertools::Itertools;
use slab::Slab; use slab::Slab;
use slotmap::SlotMap; use slotmap::SlotMap;
#[derive(Default)] #[derive(Default)]
pub struct Page { pub struct Page {
battery: Battery, battery: Battery,
connected_devices: Vec<ConnectedDevice>,
} }
impl page::Page<crate::pages::Message> for Page { impl page::Page<crate::pages::Message> for Page {
@ -29,6 +34,7 @@ impl page::Page<crate::pages::Message> for Page {
) -> Option<page::Content> { ) -> Option<page::Content> {
Some(vec![ Some(vec![
sections.insert(battery_info()), sections.insert(battery_info()),
sections.insert(connected_devices()),
sections.insert(profiles()), sections.insert(profiles()),
]) ])
} }
@ -38,11 +44,18 @@ impl page::Page<crate::pages::Message> for Page {
_page: cosmic_settings_page::Entity, _page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>, _sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> cosmic::Command<crate::pages::Message> { ) -> cosmic::Command<crate::pages::Message> {
cosmic::command::future(async move { let futures: Vec<Command<Message>> = vec![
let battery = Battery::update_battery().await; cosmic::command::future(async move {
Message::UpdateBattery(battery) let battery = Battery::update_battery().await;
}) Message::UpdateBattery(battery)
.map(crate::pages::Message::Power) }),
cosmic::command::future(async move {
let devices = ConnectedDevice::update_connected_devices().await;
Message::UpdateConnectedDevices(devices)
}),
];
cosmic::command::batch(futures).map(crate::pages::Message::Power)
} }
} }
@ -50,6 +63,7 @@ impl page::Page<crate::pages::Message> for Page {
pub enum Message { pub enum Message {
PowerProfileChange(PowerProfile), PowerProfileChange(PowerProfile),
UpdateBattery(Battery), UpdateBattery(Battery),
UpdateConnectedDevices(Vec<ConnectedDevice>),
} }
impl Page { impl Page {
@ -65,6 +79,9 @@ impl Page {
} }
} }
Message::UpdateBattery(battery) => self.battery = battery, Message::UpdateBattery(battery) => self.battery = battery,
Message::UpdateConnectedDevices(connected_devices) => {
self.connected_devices = connected_devices;
}
}; };
} }
} }
@ -78,13 +95,14 @@ fn battery_info() -> Section<crate::pages::Message> {
.show_while::<Page>(|page| page.battery.is_present) .show_while::<Page>(|page| page.battery.is_present)
.view::<Page>(move |_binder, page, section| { .view::<Page>(move |_binder, page, section| {
let battery_icon = widget::icon::from_name(page.battery.icon_name.clone()); let battery_icon = widget::icon::from_name(page.battery.icon_name.clone());
let battery_label = text::body(format!( let remaining_time = page.battery.remaining_time();
"{}% {}", let battery_label = text::body(if remaining_time.is_empty() {
page.battery.percent, format!("{}%", page.battery.percent)
page.battery.remaining_time() } else {
)); format!("{}% ({})", page.battery.percent, remaining_time)
});
column::with_capacity(2) widget::column::with_capacity(2)
.push(text::heading(&section.title)) .push(text::heading(&section.title))
.push( .push(
row!(battery_icon, battery_label) row!(battery_icon, battery_label)
@ -95,6 +113,83 @@ fn battery_info() -> Section<crate::pages::Message> {
}) })
} }
fn connected_devices() -> Section<crate::pages::Message> {
let descriptions = Slab::new();
Section::default()
.title(fl!("connected-devices"))
.descriptions(descriptions)
.show_while::<Page>(|page| !page.connected_devices.is_empty())
.view::<Page>(move |_binder, page, section| {
let devices: Vec<cosmic::Element<'_, _>> = page
.connected_devices
.iter()
.map(|connected_device| {
let battery_icon =
widget::icon::from_name(connected_device.battery.icon_name.clone());
let battery_percent_and_time = widget::text(
if connected_device.battery.remaining_duration > TimeDelta::zero() {
format!(
"{}% - {}",
connected_device.battery.percent,
&connected_device.battery.remaining_time()
)
} else {
format!("{}%", connected_device.battery.percent)
},
);
widget::container(
row!(
widget::icon::from_name(connected_device.device_icon).size(48),
column!(
text::heading(&connected_device.model),
row!(battery_icon, battery_percent_and_time)
.spacing(4)
.align_items(Alignment::Center),
)
.height(Length::Shrink)
)
.align_items(Alignment::Center)
.spacing(16)
.padding([8, 16])
.width(Length::Fill)
.height(Length::Fill),
)
.height(64)
.style(cosmic::theme::Container::List)
.into()
})
.collect();
widget::column::with_capacity(2)
.spacing(8)
.push(text::heading(&section.title))
.push(
widget::column()
.extend(
devices
.into_iter()
.chunks(2)
.into_iter()
.map(|mut device_row| {
row!(
device_row.next().unwrap_or(
widget::horizontal_space(Length::Fill).into()
),
device_row.next().unwrap_or(
widget::horizontal_space(Length::Fill).into()
),
)
.spacing(8)
}),
)
.spacing(8),
)
.into()
})
}
fn profiles() -> Section<crate::pages::Message> { fn profiles() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new(); let mut descriptions = Slab::new();
@ -119,7 +214,7 @@ fn profiles() -> Section<crate::pages::Message> {
.into_iter() .into_iter()
.map(|profile| { .map(|profile| {
settings::item_row(vec![radio( settings::item_row(vec![radio(
column::with_capacity(2) widget::column::with_capacity(2)
.push(text::body(profile.title())) .push(text::body(profile.title()))
.push(text::caption(profile.description())), .push(text::caption(profile.description())),
profile.clone(), profile.clone(),

View file

@ -318,10 +318,13 @@ battery = Battery
} }
.less-than-minute = Less than a minute .less-than-minute = Less than a minute
.and = and .and = and
.remaining-time = ({ $time } until { $action -> .remaining-time = { $time } until { $action ->
[full] full [full] full
*[other] empty *[other] empty
}) }
connected-devices = Connected Devices
.unknown = Unknown device
power-mode = Power Mode power-mode = Power Mode
.battery = Extended battery life .battery = Extended battery life

View file

@ -308,10 +308,13 @@ battery = Batterie
} }
.less-than-minute = Moins d'une minute .less-than-minute = Moins d'une minute
.and = et .and = et
.remaining-time = ({ $time } jusqu'à la { $action -> .remaining-time = { $time } jusqu'à la { $action ->
[full] charge [full] charge
*[other] decharge *[other] decharge
} complète) } complète
connected-devices = Périphériques connectés
.unknown = Périphériques inconnu
power-profiles = Modes d'énergie power-profiles = Modes d'énergie
.battery = Économie d'énergie .battery = Économie d'énergie

View file

@ -297,7 +297,7 @@ power = Alimentazione e batteria
.desc = Gestione impostazioni energetiche .desc = Gestione impostazioni energetiche
battery = Batteria battery = Batteria
.remaining-time = ({ $time } rimasti) .remaining-time = { $time } rimasti
power-mode = Power Mode power-mode = Power Mode
.battery = Estendi la vita della batteria .battery = Estendi la vita della batteria

View file

@ -308,7 +308,7 @@ power = Zasilanie
.desc = Zarządzaj ustawieniami zasilania .desc = Zarządzaj ustawieniami zasilania
battery = Bateria battery = Bateria
.remaining-time = ({ $time } pozostało) .remaining-time = { $time } pozostało
power-mode = Profile Zasilania power-mode = Profile Zasilania
.performance = Tryb Wysokowydajny .performance = Tryb Wysokowydajny

View file

@ -294,7 +294,7 @@ power = Energia & Bateria
.desc = Gere as configurações da energia .desc = Gere as configurações da energia
battery = Bateria battery = Bateria
.remaining-time = ({ $time } restante/s) .remaining-time = { $time } restante/s
power-mode = Modo de Energia power-mode = Modo de Energia
.battery = Expande a vida da bateria .battery = Expande a vida da bateria