i3status_rs/blocks/battery.rs
1//! Information about the internal power supply
2//!
3//! This block can display the current battery state (Full, Charging or Discharging), percentage
4//! charged and estimate time until (dis)charged for an internal power supply.
5//!
6//! # Configuration
7//!
8//! Key | Values | Default
9//! ----|--------|--------
10//! `device` | sysfs/UPower: The device in `/sys/class/power_supply/` to read from (can also be "DisplayDevice" for UPower, which is a single logical power source representing all physical power sources. This is for example useful if your system has multiple batteries, in which case the DisplayDevice behaves as if you had a single larger battery.). apc_ups: IPv4Address:port or hostname:port | sysfs: the first battery device found in /sys/class/power_supply, with "BATx" or "CMBx" entries taking precedence. apc_ups: "localhost:3551". upower: `DisplayDevice`
11//! `driver` | One of `"sysfs"`, `"apc_ups"`, or `"upower"` | `"sysfs"`
12//! `model` | If present, the contents of `/sys/class/power_supply/.../model_name` must match this value. Typical use is to select by model name on devices that change their path. | N/A
13//! `interval` | Update interval, in seconds. Only relevant for driver = "sysfs" or "apc_ups". | `10`
14//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $percentage "`
15//! `full_format` | Same as `format` but for when the battery is full | `" $icon "`
16//! `charging_format` | Same as `format` but for when the battery is charging | Links to `format`
17//! `empty_format` | Same as `format` but for when the battery is empty | `" $icon "`
18//! `not_charging_format` | Same as `format` but for when the battery is not charging. Defaults to the full battery icon as many batteries report this status when they are full. | `" $icon "`
19//! `missing_format` | Same as `format` if the battery cannot be found. | `" $icon "`
20//! `info` | Minimum battery level, where state is set to info | `60`
21//! `good` | Minimum battery level, where state is set to good | `60`
22//! `warning` | Minimum battery level, where state is set to warning | `30`
23//! `critical` | Minimum battery level, where state is set to critical | `15`
24//! `full_threshold` | Percentage above which the battery is considered full (`full_format` shown) | `95`
25//! `empty_threshold` | Percentage below which the battery is considered empty | `7.5`
26//!
27//! Placeholder | Value | Type | Unit
28//! -------------|-------------------------------------------------------------------------|-------------------|-----
29//! `icon` | Icon based on battery's state | Icon | -
30//! `percentage` | Battery level, in percent | Number | Percents
31//! `time_remaining` | Time remaining until (dis)charge is complete. Presented only if battery's status is (dis)charging. | Duration | -
32//! `time` | Time remaining until (dis)charge is complete. Presented only if battery's status is (dis)charging. | String *DEPRECATED* | -
33//! `power` | Power consumption by the battery or from the power supply when charging | String or Float | Watts
34//!
35//! `time` has been deprecated in favor of `time_remaining`.
36//!
37//! # Examples
38//!
39//! Basic usage:
40//!
41//! ```toml
42//! [[block]]
43//! block = "battery"
44//! format = " $icon $percentage "
45//! ```
46//!
47//! ```toml
48//! [[block]]
49//! block = "battery"
50//! format = " $percentage {$time_remaining.dur(hms:true, min_unit:m) |}"
51//! device = "DisplayDevice"
52//! driver = "upower"
53//! ```
54//!
55//! Hide missing battery:
56//!
57//! ```toml
58//! [[block]]
59//! block = "battery"
60//! missing_format = ""
61//! ```
62//!
63//! # Icons Used
64//! - `bat` (as a progression)
65//! - `bat_charging` (as a progression)
66//! - `bat_not_available`
67
68use regex::Regex;
69use std::convert::Infallible;
70use std::str::FromStr;
71
72use super::prelude::*;
73
74mod apc_ups;
75mod sysfs;
76mod upower;
77
78// make_log_macro!(debug, "battery");
79
80#[derive(Deserialize, Debug, SmartDefault)]
81#[serde(deny_unknown_fields, default)]
82pub struct Config {
83 pub device: Option<String>,
84 pub driver: BatteryDriver,
85 pub model: Option<String>,
86 #[default(10.into())]
87 pub interval: Seconds,
88 pub format: FormatConfig,
89 pub full_format: FormatConfig,
90 pub charging_format: FormatConfig,
91 pub empty_format: FormatConfig,
92 pub not_charging_format: FormatConfig,
93 pub missing_format: FormatConfig,
94 #[default(60.0)]
95 pub info: f64,
96 #[default(60.0)]
97 pub good: f64,
98 #[default(30.0)]
99 pub warning: f64,
100 #[default(15.0)]
101 pub critical: f64,
102 #[default(95.0)]
103 pub full_threshold: f64,
104 #[default(7.5)]
105 pub empty_threshold: f64,
106}
107
108#[derive(Deserialize, Debug, SmartDefault)]
109#[serde(rename_all = "snake_case")]
110pub enum BatteryDriver {
111 #[default]
112 Sysfs,
113 ApcUps,
114 Upower,
115}
116
117pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
118 let format = config.format.with_default(" $icon $percentage ")?;
119 let format_full = config.full_format.with_default(" $icon ")?;
120 let charging_format = config.charging_format.with_default_format(&format);
121 let format_empty = config.empty_format.with_default(" $icon ")?;
122 let format_not_charging = config.not_charging_format.with_default(" $icon ")?;
123 let missing_format = config.missing_format.with_default(" $icon ")?;
124
125 let dev_name = DeviceName::new(config.device.clone())?;
126 let mut device: Box<dyn BatteryDevice + Send + Sync> = match config.driver {
127 BatteryDriver::Sysfs => Box::new(sysfs::Device::new(
128 dev_name,
129 config.model.clone(),
130 config.interval,
131 )),
132 BatteryDriver::ApcUps => Box::new(apc_ups::Device::new(dev_name, config.interval).await?),
133 BatteryDriver::Upower => {
134 Box::new(upower::Device::new(dev_name, config.model.clone()).await?)
135 }
136 };
137
138 loop {
139 let mut info = device.get_info().await?;
140
141 if let Some(info) = &mut info {
142 if info.capacity >= config.full_threshold {
143 info.status = BatteryStatus::Full;
144 } else if info.capacity <= config.empty_threshold
145 && info.status != BatteryStatus::Charging
146 {
147 info.status = BatteryStatus::Empty;
148 }
149 }
150
151 match info {
152 Some(info) => {
153 let mut widget = Widget::new();
154
155 widget.set_format(match info.status {
156 BatteryStatus::Empty => format_empty.clone(),
157 BatteryStatus::Full => format_full.clone(),
158 BatteryStatus::Charging => charging_format.clone(),
159 BatteryStatus::NotCharging => format_not_charging.clone(),
160 _ => format.clone(),
161 });
162
163 let mut values = map!(
164 "percentage" => Value::percents(info.capacity)
165 );
166
167 info.power
168 .map(|p| values.insert("power".into(), Value::watts(p)));
169 info.time_remaining.inspect(|&t| {
170 map! { @extend values
171 "time" => Value::text(
172 format!(
173 "{}:{:02}",
174 (t / 3600.) as i32,
175 (t % 3600. / 60.) as i32
176 ),
177 ),
178 "time_remaining" => Value::duration(
179 Duration::from_secs(t as u64),
180 ),
181 }
182 });
183
184 let (icon_name, icon_value, state) = match (info.status, info.capacity) {
185 (BatteryStatus::Empty, _) => ("bat", 0.0, State::Critical),
186 (BatteryStatus::Full | BatteryStatus::NotCharging, _) => {
187 ("bat", 1.0, State::Idle)
188 }
189 (status, capacity) => (
190 if status == BatteryStatus::Charging {
191 "bat_charging"
192 } else {
193 "bat"
194 },
195 capacity / 100.0,
196 if status == BatteryStatus::Charging {
197 State::Good
198 } else if capacity <= config.critical {
199 State::Critical
200 } else if capacity <= config.warning {
201 State::Warning
202 } else if capacity <= config.info {
203 State::Info
204 } else if capacity > config.good {
205 State::Good
206 } else {
207 State::Idle
208 },
209 ),
210 };
211
212 values.insert(
213 "icon".into(),
214 Value::icon_progression(icon_name, icon_value),
215 );
216
217 widget.set_values(values);
218 widget.state = state;
219 api.set_widget(widget)?;
220 }
221 None => {
222 let mut widget = Widget::new()
223 .with_format(missing_format.clone())
224 .with_state(State::Critical);
225 widget.set_values(map!("icon" => Value::icon("bat_not_available")));
226 api.set_widget(widget)?;
227 }
228 }
229
230 select! {
231 update = device.wait_for_change() => update?,
232 _ = api.wait_for_update_request() => (),
233 }
234 }
235}
236
237#[async_trait]
238trait BatteryDevice {
239 async fn get_info(&mut self) -> Result<Option<BatteryInfo>>;
240 async fn wait_for_change(&mut self) -> Result<()>;
241}
242
243/// `Option<Regex>`, but more intuitive
244#[derive(Debug)]
245enum DeviceName {
246 Any,
247 Regex(Regex),
248}
249
250impl DeviceName {
251 fn new(pat: Option<String>) -> Result<Self> {
252 Ok(match pat {
253 None => Self::Any,
254 Some(pat) => Self::Regex(pat.parse().error("failed to parse regex")?),
255 })
256 }
257
258 fn matches(&self, name: &str) -> bool {
259 match self {
260 Self::Any => true,
261 Self::Regex(pat) => pat.is_match(name),
262 }
263 }
264
265 fn exact(&self) -> Option<&str> {
266 match self {
267 Self::Any => None,
268 Self::Regex(pat) => Some(pat.as_str()),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Copy)]
274struct BatteryInfo {
275 /// Current status, e.g. "charging", "discharging", etc.
276 status: BatteryStatus,
277 /// The capacity in percents
278 capacity: f64,
279 /// Power consumption in watts
280 power: Option<f64>,
281 /// Time in seconds
282 time_remaining: Option<f64>,
283}
284
285#[derive(Copy, Clone, Debug, Eq, PartialEq, SmartDefault)]
286enum BatteryStatus {
287 Charging,
288 Discharging,
289 Empty,
290 Full,
291 NotCharging,
292 #[default]
293 Unknown,
294}
295
296impl FromStr for BatteryStatus {
297 type Err = Infallible;
298
299 fn from_str(s: &str) -> Result<Self, Self::Err> {
300 Ok(match s {
301 "Charging" => Self::Charging,
302 "Discharging" => Self::Discharging,
303 "Empty" => Self::Empty,
304 "Full" => Self::Full,
305 "Not charging" => Self::NotCharging,
306 _ => Self::Unknown,
307 })
308 }
309}