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}