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