i3status_rs/blocks/battery/
apc_ups.rs

1use std::str::FromStr;
2use tokio::net::TcpStream;
3use tokio::time::Interval;
4
5use super::{BatteryDevice, BatteryInfo, BatteryStatus, DeviceName};
6use crate::blocks::prelude::*;
7
8#[derive(Debug, Default)]
9struct PropertyMap(HashMap<String, String>);
10
11make_log_macro!(debug, "battery[apc_ups]");
12
13impl PropertyMap {
14    fn insert(&mut self, k: String, v: String) -> Option<String> {
15        self.0.insert(k, v)
16    }
17
18    fn get(&self, k: &str) -> Option<&str> {
19        self.0.get(k).map(|v| v.as_str())
20    }
21
22    fn get_property<T: FromStr + Send + Sync>(
23        &self,
24        property_name: &str,
25        required_unit: &str,
26    ) -> Result<T> {
27        let stat = self
28            .get(property_name)
29            .or_error(|| format!("{property_name} not in apc ups data"))?;
30        let (value, unit) = stat
31            .split_once(' ')
32            .or_error(|| format!("could not split {property_name}"))?;
33        if unit == required_unit {
34            value
35                .parse::<T>()
36                .map_err(|_| Error::new("Could not parse data"))
37        } else {
38            Err(Error::new(format!(
39                "Expected unit for {property_name} are {required_unit}, but got {unit}"
40            )))
41        }
42    }
43}
44
45#[derive(Debug)]
46struct ApcConnection(TcpStream);
47
48impl ApcConnection {
49    async fn connect(addr: &str) -> Result<Self> {
50        Ok(Self(
51            TcpStream::connect(addr)
52                .await
53                .error("Failed to connect to socket")?,
54        ))
55    }
56
57    async fn write(&mut self, msg: &[u8]) -> Result<()> {
58        let msg_len = u16::try_from(msg.len())
59            .error("msg is too long, it must be less than 2^16 characters long")?;
60
61        self.0
62            .write_u16(msg_len)
63            .await
64            .error("Could not write message length to socket")?;
65        self.0
66            .write_all(msg)
67            .await
68            .error("Could not write message to socket")?;
69        Ok(())
70    }
71
72    async fn read_line<'a>(&'_ mut self, buf: &'a mut Vec<u8>) -> Result<Option<&'a str>> {
73        let read_size = self
74            .0
75            .read_u16()
76            .await
77            .error("Could not read response length from socket")?
78            .into();
79        if read_size == 0 {
80            return Ok(None);
81        }
82
83        buf.resize(read_size, 0);
84        self.0
85            .read_exact(buf)
86            .await
87            .error("Could not read from socket")?;
88
89        std::str::from_utf8(buf).error("invalid UTF8").map(Some)
90    }
91}
92
93pub(super) struct Device {
94    addr: String,
95    interval: Interval,
96}
97
98impl Device {
99    pub(super) async fn new(dev_name: DeviceName, interval: Seconds) -> Result<Self> {
100        let addr = dev_name.exact().unwrap_or("localhost:3551");
101        Ok(Self {
102            addr: addr.to_string(),
103            interval: interval.timer(),
104        })
105    }
106
107    async fn get_status(&mut self) -> Result<PropertyMap> {
108        let mut conn = ApcConnection::connect(&self.addr).await?;
109
110        conn.write(b"status").await?;
111
112        let mut buf = vec![];
113        let mut property_map = PropertyMap::default();
114
115        while let Some(line) = conn.read_line(&mut buf).await? {
116            if let Some((key, value)) = line.split_once(':') {
117                property_map.insert(key.trim().to_string(), value.trim().to_string());
118            }
119        }
120
121        Ok(property_map)
122    }
123}
124
125#[async_trait]
126impl BatteryDevice for Device {
127    async fn get_info(&mut self) -> Result<Option<BatteryInfo>> {
128        let status_data = self
129            .get_status()
130            .await
131            .map_err(|e| {
132                debug!("{e}");
133                e
134            })
135            .unwrap_or_default();
136
137        let status_str = status_data.get("STATUS").unwrap_or("COMMLOST");
138
139        // Even if the connection is valid, in the first few seconds
140        // after apcupsd starts BCHARGE may not be present
141        let capacity = status_data
142            .get_property::<f64>("BCHARGE", "Percent")
143            .unwrap_or(f64::MIN);
144
145        if status_str == "COMMLOST" || capacity == f64::MIN {
146            return Ok(None);
147        }
148
149        let status = if status_str == "ONBATT" {
150            if capacity == 0.0 {
151                BatteryStatus::Empty
152            } else {
153                BatteryStatus::Discharging
154            }
155        } else if status_str == "ONLINE" {
156            if capacity == 100.0 {
157                BatteryStatus::Full
158            } else {
159                BatteryStatus::Charging
160            }
161        } else {
162            BatteryStatus::Unknown
163        };
164
165        let power = status_data
166            .get_property::<f64>("NOMPOWER", "Watts")
167            .ok()
168            .and_then(|nominal_power| {
169                status_data
170                    .get_property::<f64>("LOADPCT", "Percent")
171                    .ok()
172                    .map(|load_percent| nominal_power * load_percent / 100.0)
173            });
174
175        let time_remaining = status_data
176            .get_property::<f64>("TIMELEFT", "Minutes")
177            .ok()
178            .map(|e| e * 60_f64);
179
180        Ok(Some(BatteryInfo {
181            status,
182            capacity,
183            power,
184            time_remaining,
185        }))
186    }
187
188    async fn wait_for_change(&mut self) -> Result<()> {
189        self.interval.tick().await;
190        Ok(())
191    }
192}