i3status_rs/blocks/battery/
upower.rs

1use tokio::try_join;
2use zbus::fdo::{PropertiesChangedStream, PropertiesProxy};
3use zbus::{Connection, zvariant};
4use zvariant::ObjectPath;
5
6use super::{BatteryDevice, BatteryInfo, BatteryStatus, DeviceName};
7use crate::blocks::prelude::*;
8use crate::util::new_system_dbus_connection;
9
10const DISPLAY_DEVICE_PATH: ObjectPath =
11    ObjectPath::from_static_str_unchecked("/org/freedesktop/UPower/devices/DisplayDevice");
12
13struct DeviceConnection {
14    device_proxy: DeviceProxy<'static>,
15    changes: PropertiesChangedStream,
16}
17
18impl DeviceConnection {
19    async fn new(
20        dbus_conn: &Connection,
21        device: &DeviceName,
22        expected_model: Option<&str>,
23    ) -> Result<Option<Self>> {
24        let device_proxy =
25            if device.exact().is_none_or(|d| d == "DisplayDevice") && expected_model.is_none() {
26                DeviceProxy::builder(dbus_conn)
27                    .path(DISPLAY_DEVICE_PATH)
28                    .unwrap()
29                    .build()
30                    .await
31                    .error("Failed to create DeviceProxy")?
32            } else {
33                let mut res = None;
34                for path in UPowerProxy::new(dbus_conn)
35                    .await
36                    .error("Failed to create UPowerProxy")?
37                    .enumerate_devices()
38                    .await
39                    .error("Failed to retrieve UPower devices")?
40                {
41                    let proxy = DeviceProxy::builder(dbus_conn)
42                        .path(path)
43                        .unwrap()
44                        .build()
45                        .await
46                        .error("Failed to create DeviceProxy")?;
47
48                    // Filter by model if needed
49                    if let Some(expected_model) = &expected_model {
50                        if let Ok(device_model) = proxy.model().await {
51                            if !expected_model.eq(&device_model) {
52                                continue;
53                            }
54                        }
55                    }
56                    // Verify device type
57                    // https://upower.freedesktop.org/docs/Device.html#Device:Type
58                    // consider any peripheral, UPS and internal battery
59                    let device_type = proxy.type_().await.error("Failed to get device's type")?;
60                    if device_type == 1 {
61                        continue;
62                    }
63                    let name = proxy
64                        .native_path()
65                        .await
66                        .error("Failed to get device's native path")?;
67                    if device.matches(&name) {
68                        res = Some(proxy);
69                        break;
70                    }
71                }
72                match res {
73                    Some(res) => res,
74                    None => return Ok(None),
75                }
76            };
77
78        let changes = PropertiesProxy::builder(dbus_conn)
79            .destination("org.freedesktop.UPower")
80            .unwrap()
81            .path(device_proxy.inner().path().to_owned())
82            .unwrap()
83            .build()
84            .await
85            .error("Failed to create PropertiesProxy")?
86            .receive_properties_changed()
87            .await
88            .error("Failed to create PropertiesChangedStream")?;
89
90        Ok(Some(DeviceConnection {
91            device_proxy,
92            changes,
93        }))
94    }
95}
96
97pub(super) struct Device {
98    dbus_conn: Connection,
99    device: DeviceName,
100    dev_model: Option<String>,
101    device_conn: Option<DeviceConnection>,
102    device_added_stream: DeviceAddedStream,
103    device_removed_stream: DeviceRemovedStream,
104}
105
106impl Device {
107    pub(super) async fn new(device: DeviceName, dev_model: Option<String>) -> Result<Self> {
108        let dbus_conn = new_system_dbus_connection().await?;
109
110        let device_conn = DeviceConnection::new(&dbus_conn, &device, dev_model.as_deref()).await?;
111
112        let upower_proxy = UPowerProxy::new(&dbus_conn)
113            .await
114            .error("Could not create UPowerProxy")?;
115
116        let (device_added_stream, device_removed_stream) = try_join! {
117            upower_proxy.receive_device_added(),
118            upower_proxy.receive_device_removed()
119        }
120        .error("Could not create signal stream")?;
121
122        Ok(Self {
123            dbus_conn,
124            device,
125            dev_model,
126            device_conn,
127            device_added_stream,
128            device_removed_stream,
129        })
130    }
131}
132
133#[async_trait]
134impl BatteryDevice for Device {
135    async fn get_info(&mut self) -> Result<Option<BatteryInfo>> {
136        match &self.device_conn {
137            None => Ok(None),
138            Some(device_conn) => {
139                match try_join! {
140                    device_conn.device_proxy.percentage(),
141                    device_conn.device_proxy.energy_rate(),
142                    device_conn.device_proxy.state(),
143                    device_conn.device_proxy.time_to_full(),
144                    device_conn.device_proxy.time_to_empty(),
145                } {
146                    Err(_) => Ok(None),
147                    Ok((capacity, power, state, time_to_full, time_to_empty)) => {
148                        let status = match state {
149                            1 => BatteryStatus::Charging,
150                            2 | 6 => BatteryStatus::Discharging,
151                            3 => BatteryStatus::Empty,
152                            4 => BatteryStatus::Full,
153                            5 => BatteryStatus::NotCharging,
154                            _ => BatteryStatus::Unknown,
155                        };
156
157                        let time_remaining = match status {
158                            BatteryStatus::Charging => Some(time_to_full as f64),
159                            BatteryStatus::Discharging => Some(time_to_empty as f64),
160                            _ => None,
161                        };
162
163                        Ok(Some(BatteryInfo {
164                            status,
165                            capacity,
166                            power: Some(power),
167                            time_remaining,
168                        }))
169                    }
170                }
171            }
172        }
173    }
174
175    async fn wait_for_change(&mut self) -> Result<()> {
176        match &mut self.device_conn {
177            Some(device_conn) => loop {
178                select! {
179                    _ = self.device_added_stream.next() => {},
180                    _ = device_conn.changes.next() => {
181                        break;
182                    },
183                    Some(msg) = self.device_removed_stream.next() => {
184                        let args = msg.args().unwrap();
185                        if args.device().as_ref() == device_conn.device_proxy.inner().path().as_ref() {
186                            self.device_conn = None;
187                            break;
188                        }
189                    },
190                }
191            },
192            None => loop {
193                select! {
194                    _ = self.device_removed_stream.next() => {},
195                    _ = self.device_added_stream.next() => {
196                        if let Some(device_conn) =
197                        DeviceConnection::new(&self.dbus_conn, &self.device, self.dev_model.as_deref()).await?
198                        {
199                            self.device_conn = Some(device_conn);
200                            break;
201                        }
202                    },
203                }
204            },
205        }
206
207        Ok(())
208    }
209}
210
211#[zbus::proxy(
212    interface = "org.freedesktop.UPower.Device",
213    default_service = "org.freedesktop.UPower"
214)]
215trait Device {
216    #[zbus(property)]
217    fn energy_rate(&self) -> zbus::Result<f64>;
218
219    #[zbus(property)]
220    fn is_present(&self) -> zbus::Result<bool>;
221
222    #[zbus(property)]
223    fn native_path(&self) -> zbus::Result<String>;
224
225    #[zbus(property)]
226    fn model(&self) -> zbus::Result<String>;
227
228    #[zbus(property)]
229    fn online(&self) -> zbus::Result<bool>;
230
231    #[zbus(property)]
232    fn percentage(&self) -> zbus::Result<f64>;
233
234    #[zbus(property)]
235    fn state(&self) -> zbus::Result<u32>;
236
237    #[zbus(property)]
238    fn time_to_empty(&self) -> zbus::Result<i64>;
239
240    #[zbus(property)]
241    fn time_to_full(&self) -> zbus::Result<i64>;
242
243    #[zbus(property, name = "Type")]
244    fn type_(&self) -> zbus::Result<u32>;
245}
246
247#[zbus::proxy(
248    interface = "org.freedesktop.UPower",
249    default_service = "org.freedesktop.UPower",
250    default_path = "/org/freedesktop/UPower"
251)]
252trait UPower {
253    fn enumerate_devices(&self) -> zbus::Result<Vec<zvariant::OwnedObjectPath>>;
254
255    fn get_display_device(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
256
257    #[zbus(signal)]
258    fn device_added(&self, device: zvariant::OwnedObjectPath) -> zbus::Result<()>;
259
260    #[zbus(signal)]
261    fn device_removed(&self, device: zvariant::OwnedObjectPath) -> zbus::Result<()>;
262
263    #[zbus(property)]
264    fn on_battery(&self) -> zbus::Result<bool>;
265}