i3status_rs/blocks/battery/
apc_ups.rs1use bytes::Bytes;
2use futures::SinkExt as _;
3
4use serde::de;
5use tokio::net::TcpStream;
6use tokio::time::Interval;
7use tokio_util::codec::{Framed, LengthDelimitedCodec};
8
9use super::{BatteryDevice, BatteryInfo, BatteryStatus, DeviceName};
10use crate::blocks::prelude::*;
11
12make_log_macro!(debug, "battery[apc_ups]");
13
14#[derive(Debug, SmartDefault)]
15enum Value {
16 String(String),
17 Percent(f64),
19 Watts(f64),
20 Seconds(f64),
21 #[default]
22 None,
23}
24
25impl<'de> Deserialize<'de> for Value {
26 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
27 where
28 D: de::Deserializer<'de>,
29 {
30 let s = String::deserialize(deserializer)?;
31 for unit in ["Percent", "Watts", "Seconds", "Minutes", "Hours"] {
32 if let Some(stripped) = s.strip_suffix(unit) {
33 let value = stripped.trim().parse::<f64>().map_err(de::Error::custom)?;
34 return Ok(match unit {
35 "Percent" => Value::Percent(value),
36 "Watts" => Value::Watts(value),
37 "Seconds" => Value::Seconds(value),
38 "Minutes" => Value::Seconds(value * 60.0),
39 "Hours" => Value::Seconds(value * 3600.0),
40 _ => unreachable!(),
41 });
42 }
43 }
44 Ok(Value::String(s))
45 }
46}
47
48#[derive(Debug, Deserialize, Default)]
49#[serde(rename_all = "UPPERCASE", default)]
50struct Properties {
51 status: Value,
52 bcharge: Value,
53 nompower: Value,
54 loadpct: Value,
55 timeleft: Value,
56}
57
58pub(super) struct Device {
59 addr: String,
60 interval: Interval,
61}
62
63impl Device {
64 pub(super) async fn new(dev_name: DeviceName, interval: Seconds) -> Result<Self> {
65 let addr = dev_name.exact().unwrap_or("localhost:3551");
66 Ok(Self {
67 addr: addr.to_string(),
68 interval: interval.timer(),
69 })
70 }
71
72 async fn get_status(&mut self) -> Result<Properties> {
73 let mut conn = Framed::new(
74 TcpStream::connect(&self.addr)
75 .await
76 .error("Failed to connect to socket")?,
77 LengthDelimitedCodec::builder()
78 .length_field_type::<u16>()
79 .new_codec(),
80 );
81
82 conn.send(Bytes::from_static(b"status"))
83 .await
84 .error("Could not send message to socket")?;
85 conn.close().await.error("Could not close socket sink")?;
86
87 let mut map = serde_json::Map::new();
88
89 while let Some(frame) = conn.next().await {
90 let frame = frame.error("Failed to read from socket")?;
91 if frame.is_empty() {
92 continue;
93 }
94 let line = std::str::from_utf8(&frame).error("Failed to convert to UTF-8")?;
95 let Some((key, value)) = line.split_once(':') else {
96 debug!("Invalid field format: {line:?}");
97 continue;
98 };
99 map.insert(
100 key.trim().to_uppercase(),
101 serde_json::Value::String(value.trim().to_string()),
102 );
103 }
104
105 serde_json::from_value(serde_json::Value::Object(map)).error("Failed to deserialize")
106 }
107}
108
109#[async_trait]
110impl BatteryDevice for Device {
111 async fn get_info(&mut self) -> Result<Option<BatteryInfo>> {
112 let status_data = self
113 .get_status()
114 .await
115 .map_err(|e| {
116 debug!("{e}");
117 e
118 })
119 .unwrap_or_default();
120
121 let Value::String(status_str) = status_data.status else {
122 return Ok(None);
123 };
124
125 let status = match &*status_str {
126 "ONBATT" => BatteryStatus::Discharging,
127 "ONLINE" => BatteryStatus::Charging,
128 _ => BatteryStatus::Unknown,
129 };
130
131 let Value::Percent(capacity) = status_data.bcharge else {
134 return Ok(None);
135 };
136
137 let power = match (status_data.nompower, status_data.loadpct) {
138 (Value::Watts(nominal_power), Value::Percent(load_percent)) => {
139 Some(nominal_power * load_percent / 100.0)
140 }
141 _ => None,
142 };
143
144 let time_remaining = match status_data.timeleft {
145 Value::Seconds(time_left) => Some(time_left),
146 _ => None,
147 };
148
149 Ok(Some(BatteryInfo {
150 status,
151 capacity,
152 power,
153 time_remaining,
154 }))
155 }
156
157 async fn wait_for_change(&mut self) -> Result<()> {
158 self.interval.tick().await;
159 Ok(())
160 }
161}