i3status_rs/blocks/battery/
apc_ups.rs1use 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 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}