i3status_rs/blocks/battery/
apc_ups.rs

1use 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    // The value is a percentage (0-100)
18    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        // Even if the connection is valid, in the first few seconds
132        // after apcupsd starts BCHARGE may not be present
133        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}