use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use dbus::arg::Array;
use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties;
use serde_derive::Deserialize;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::de::deserialize_duration;
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::scheduler::Task;
use crate::util::{battery_level_to_icon, read_file};
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, Spacing, State};
pub trait BatteryDevice {
fn is_available(&self) -> bool;
fn refresh_device_info(&mut self) -> Result<()>;
fn status(&self) -> Result<String>;
fn capacity(&self) -> Result<u64>;
fn time_remaining(&self) -> Result<u64>;
fn power_consumption(&self) -> Result<u64>;
}
pub struct PowerSupplyDevice {
device_path: PathBuf,
allow_missing: bool,
charge_full: Option<u64>,
energy_full: Option<u64>,
}
impl PowerSupplyDevice {
pub fn from_device(device: &str, allow_missing: bool) -> Result<Self> {
let device_path = Path::new("/sys/class/power_supply").join(device);
let device = PowerSupplyDevice {
device_path,
allow_missing,
charge_full: None,
energy_full: None,
};
Ok(device)
}
}
impl BatteryDevice for PowerSupplyDevice {
fn is_available(&self) -> bool {
self.device_path.exists()
}
fn refresh_device_info(&mut self) -> Result<()> {
if !self.is_available() {
if self.allow_missing {
self.charge_full = None;
self.energy_full = None;
return Ok(());
}
return Err(BlockError(
"battery".into(),
format!(
"Power supply device '{}' does not exist",
self.device_path.to_string_lossy()
),
));
}
self.charge_full = if self.device_path.join("charge_full").exists() {
Some(
read_file("battery", &self.device_path.join("charge_full"))?
.parse::<u64>()
.block_error("battery", "failed to parse charge_full")?,
)
} else {
None
};
self.energy_full = if self.device_path.join("energy_full").exists() {
Some(
read_file("battery", &self.device_path.join("energy_full"))?
.parse::<u64>()
.block_error("battery", "failed to parse energy_full")?,
)
} else {
None
};
Ok(())
}
fn status(&self) -> Result<String> {
read_file("battery", &self.device_path.join("status"))
}
fn capacity(&self) -> Result<u64> {
let capacity_path = self.device_path.join("capacity");
let charge_path = self.device_path.join("charge_now");
let energy_path = self.device_path.join("energy_now");
let capacity = if capacity_path.exists() {
read_file("battery", &capacity_path)?
.parse::<u64>()
.block_error("battery", "failed to parse capacity")?
} else if charge_path.exists() && self.charge_full.is_some() {
let charge = read_file("battery", &charge_path)?
.parse::<u64>()
.block_error("battery", "failed to parse charge_now")?;
((charge as f64 / self.charge_full.unwrap() as f64) * 100.0) as u64
} else if energy_path.exists() && self.energy_full.is_some() {
let charge = read_file("battery", &energy_path)?
.parse::<u64>()
.block_error("battery", "failed to parse energy_now")?;
((charge as f64 / self.energy_full.unwrap() as f64) * 100.0) as u64
} else {
return Err(BlockError(
"battery".to_string(),
"Device does not support reading capacity, charge, or energy".to_string(),
));
};
match capacity {
0..=100 => Ok(capacity),
_ => Ok(100),
}
}
fn time_remaining(&self) -> Result<u64> {
let time_to_empty_now_path = self.device_path.join("time_to_empty_now");
let time_to_empty = if time_to_empty_now_path.exists() {
read_file("battery", &time_to_empty_now_path)?
.parse::<u64>()
.block_error("battery", "failed to parse time to empty")
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support reading time to empty directly".to_string(),
))
};
let time_to_full_now_path = self.device_path.join("time_to_full_now");
let time_to_full = if time_to_full_now_path.exists() {
read_file("battery", &time_to_full_now_path)?
.parse::<u64>()
.block_error("battery", "failed to parse time to full")
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support reading time to full directly".to_string(),
))
};
let full = if self.energy_full.is_some() {
self.energy_full
} else if self.charge_full.is_some() {
self.charge_full
} else {
None
};
let energy_path = self.device_path.join("energy_now");
let charge_path = self.device_path.join("charge_now");
let fill = if energy_path.exists() {
read_file("battery", &energy_path)?
.parse::<f64>()
.block_error("battery", "failed to parse energy_now")
} else if charge_path.exists() {
read_file("battery", &charge_path)?
.parse::<f64>()
.block_error("battery", "failed to parse charge_now")
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support reading energy".to_string(),
))
};
let power_path = self.device_path.join("power_now");
let current_path = self.device_path.join("current_now");
let usage = if power_path.exists() {
read_file("battery", &power_path)?
.parse::<f64>()
.block_error("battery", "failed to parse power_now")
} else if current_path.exists() {
read_file("battery", ¤t_path)?
.parse::<f64>()
.block_error("battery", "failed to parse current_now")
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support reading power".to_string(),
))
};
let status = self.status()?;
match status.as_str() {
"Discharging" => {
if time_to_empty.is_ok() {
time_to_empty
} else if fill.is_ok() && usage.is_ok() {
Ok(((fill.unwrap() / usage.unwrap()) * 60.0) as u64)
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support any method of calculating time to empty"
.to_string(),
))
}
}
"Charging" => {
if time_to_full.is_ok() {
time_to_full
} else if full.is_some() && fill.is_ok() && usage.is_ok() {
Ok((((full.unwrap() as f64 - fill.unwrap()) / usage.unwrap()) * 60.0) as u64)
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support any method of calculating time to full"
.to_string(),
))
}
}
_ => {
Ok(0)
}
}
}
fn power_consumption(&self) -> Result<u64> {
let power_path = self.device_path.join("power_now");
let current_path = self.device_path.join("current_now");
let voltage_path = self.device_path.join("voltage_now");
if power_path.exists() {
Ok(read_file("battery", &power_path)?
.parse::<u64>()
.block_error("battery", "failed to parse power_now")?)
} else if current_path.exists() && voltage_path.exists() {
let current = read_file("battery", ¤t_path)?
.parse::<u64>()
.block_error("battery", "failed to parse current_now")?;
let voltage = read_file("battery", &voltage_path)?
.parse::<u64>()
.block_error("battery", "failed to parse voltage_now")?;
Ok((current * voltage) / 1_000_000)
} else {
Err(BlockError(
"battery".to_string(),
"Device does not support power consumption".to_string(),
))
}
}
}
pub struct UpowerDevice {
device_path: String,
con: dbus::ffidisp::Connection,
}
impl UpowerDevice {
pub fn from_device(device: &str) -> Result<Self> {
let device_path;
let con = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System)
.block_error("battery", "Failed to establish D-Bus connection.")?;
if device == "DisplayDevice" {
device_path = String::from("/org/freedesktop/UPower/devices/DisplayDevice");
} else {
let msg = dbus::Message::new_method_call(
"org.freedesktop.UPower",
"/org/freedesktop/UPower",
"org.freedesktop.UPower",
"EnumerateDevices",
)
.block_error("battery", "Failed to create DBus message")?;
let dbus_reply = con
.send_with_reply_and_block(msg, 2000)
.block_error("battery", "Failed to retrieve DBus reply")?;
let mut paths: Array<dbus::Path, _> = dbus_reply
.get1()
.block_error("battery", "Failed to read DBus reply")?;
device_path = paths
.find(|entry| entry.ends_with(device))
.block_error("battery", "UPower device could not be found.")?
.as_cstr()
.to_string_lossy()
.into_owned();
}
let upower_type: u32 = con
.with_path("org.freedesktop.UPower", &device_path, 1000)
.get("org.freedesktop.UPower.Device", "Type")
.block_error("battery", "Failed to read UPower Type property.")?;
if upower_type == 1 {
return Err(BlockError(
"battery".into(),
"UPower device is not a battery.".into(),
));
}
Ok(UpowerDevice { device_path, con })
}
pub fn monitor(&self, id: usize, update_request: Sender<Task>) {
let path = self.device_path.clone();
thread::Builder::new()
.name("battery".into())
.spawn(move || {
let con = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System)
.expect("Failed to establish D-Bus connection.");
let rule = format!(
"type='signal',\
path='{}',\
interface='org.freedesktop.DBus.Properties',\
member='PropertiesChanged'",
path
);
con.incoming(10_000).next();
con.add_match(&rule)
.expect("Failed to add D-Bus match rule.");
loop {
if con.incoming(10_000).next().is_some() {
update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
thread::sleep(Duration::from_millis(1000))
}
}
})
.unwrap();
}
}
impl BatteryDevice for UpowerDevice {
fn is_available(&self) -> bool {
true
}
fn refresh_device_info(&mut self) -> Result<()> {
Ok(())
}
fn status(&self) -> Result<String> {
let status: u32 = self
.con
.with_path("org.freedesktop.UPower", &self.device_path, 1000)
.get("org.freedesktop.UPower.Device", "State")
.block_error("battery", "Failed to read UPower State property.")?;
match status {
1 => Ok("Charging".to_string()),
2 => Ok("Discharging".to_string()),
3 => Ok("Empty".to_string()),
4 => Ok("Full".to_string()),
5 => Ok("Not charging".to_string()),
6 => Ok("Discharging".to_string()),
_ => Ok("Unknown".to_string()),
}
}
fn capacity(&self) -> Result<u64> {
let capacity: f64 = self
.con
.with_path("org.freedesktop.UPower", &self.device_path, 1000)
.get("org.freedesktop.UPower.Device", "Percentage")
.block_error("battery", "Failed to read UPower Percentage property.")?;
if capacity > 100.0 {
Ok(100)
} else {
Ok(capacity as u64)
}
}
fn time_remaining(&self) -> Result<u64> {
let property = if self.status()? == "Charging" {
"TimeToFull"
} else {
"TimeToEmpty"
};
let time_to_empty: i64 = self
.con
.with_path("org.freedesktop.UPower", &self.device_path, 1000)
.get("org.freedesktop.UPower.Device", property)
.block_error(
"battery",
&format!("Failed to read UPower {} property.", property),
)?;
Ok((time_to_empty / 60) as u64)
}
fn power_consumption(&self) -> Result<u64> {
let energy_rate: f64 = self
.con
.with_path("org.freedesktop.UPower", &self.device_path, 1000)
.get("org.freedesktop.UPower.Device", "EnergyRate")
.block_error("battery", "Failed to read UPower EnergyRate property.")?;
Ok((energy_rate * 1_000_000.0) as u64)
}
}
pub struct Battery {
id: usize,
output: TextWidget,
update_interval: Duration,
device: Box<dyn BatteryDevice>,
format: FormatTemplate,
full_format: FormatTemplate,
missing_format: FormatTemplate,
allow_missing: bool,
hide_missing: bool,
driver: BatteryDriver,
good: u64,
info: u64,
warning: u64,
critical: u64,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum BatteryDriver {
Sysfs,
Upower,
}
impl Default for BatteryDriver {
fn default() -> Self {
BatteryDriver::Sysfs
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct BatteryConfig {
#[serde(deserialize_with = "deserialize_duration")]
pub interval: Duration,
pub device: String,
pub format: String,
pub full_format: String,
pub missing_format: String,
pub driver: BatteryDriver,
pub good: u64,
pub info: u64,
pub warning: u64,
pub critical: u64,
pub allow_missing: bool,
pub hide_missing: bool,
}
impl Default for BatteryConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(10),
device: "BAT0".to_string(),
format: "{percentage}".to_string(),
full_format: "".to_string(),
missing_format: "{percentage}".to_string(),
driver: BatteryDriver::Sysfs,
good: 60,
info: 60,
warning: 30,
critical: 15,
allow_missing: false,
hide_missing: false,
}
}
}
impl ConfigBlock for Battery {
type Config = BatteryConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
update_request: Sender<Task>,
) -> Result<Self> {
let device: Box<dyn BatteryDevice> = match block_config.driver {
BatteryDriver::Upower => {
let out = UpowerDevice::from_device(&block_config.device)?;
out.monitor(id, update_request);
Box::new(out)
}
BatteryDriver::Sysfs => Box::new(PowerSupplyDevice::from_device(
&block_config.device,
block_config.allow_missing,
)?),
};
Ok(Battery {
id,
update_interval: block_config.interval,
output: TextWidget::new(id, 0, shared_config),
device,
format: FormatTemplate::from_string(&block_config.format)?,
full_format: FormatTemplate::from_string(&block_config.full_format)?,
missing_format: FormatTemplate::from_string(&block_config.missing_format)?,
allow_missing: block_config.allow_missing,
hide_missing: block_config.hide_missing,
driver: block_config.driver,
good: block_config.good,
info: block_config.info,
warning: block_config.warning,
critical: block_config.critical,
})
}
}
impl Block for Battery {
fn update(&mut self) -> Result<Option<Update>> {
if !self.device.is_available() && self.allow_missing {
let values = map!(
"percentage" => Value::from_string("X".to_string()),
"time" => Value::from_string("xx:xx".to_string()),
"power" => Value::from_string("N/A".to_string()),
);
self.output.set_icon("bat_not_available")?;
self.output.set_text(self.missing_format.render(&values)?);
self.output.set_state(State::Warning);
return match self.driver {
BatteryDriver::Sysfs => Ok(Some(Update::Every(self.update_interval))),
BatteryDriver::Upower => Ok(None),
};
}
self.device.refresh_device_info()?;
let status = self.device.status()?;
let capacity = self.device.capacity();
let values = map!(
"percentage" => match capacity {
Ok(capacity) => Value::from_integer(capacity as i64).percents(),
_ => Value::from_string("×".into()),
},
"time" => match self.device.time_remaining() {
Ok(0) => Value::from_string("".into()),
Ok(time) => Value::from_string(format!("{}:{:02}", std::cmp::min(time / 60, 99), time % 60)),
_ => Value::from_string("×".into()),
},
"power" => match self.device.power_consumption() {
Ok(power) => Value::from_float(power as f64 * 1e-6).watts(),
_ => Value::from_string("×".into()),
},
);
if status == "Full" || status == "Not charging" {
self.output.set_icon("bat_full")?;
self.output.set_text(self.full_format.render(&values)?);
self.output.set_state(State::Good);
self.output.set_spacing(Spacing::Hidden);
} else {
self.output.set_text(self.format.render(&values)?);
match status.as_str() {
"Charging" => {
self.output.set_state(State::Good);
}
_ => {
self.output.set_state(match capacity {
Ok(capacity) => {
if capacity <= self.critical {
State::Critical
} else if capacity <= self.warning {
State::Warning
} else if capacity <= self.info {
State::Info
} else if capacity > self.good {
State::Good
} else {
State::Idle
}
}
Err(_) => State::Warning,
});
}
}
self.output.set_icon(match status.as_str() {
"Discharging" => battery_level_to_icon(capacity),
"Charging" => "bat_charging",
_ => battery_level_to_icon(capacity),
})?;
self.output.set_spacing(Spacing::Normal);
}
match self.driver {
BatteryDriver::Sysfs => Ok(Some(self.update_interval.into())),
BatteryDriver::Upower => Ok(None),
}
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
if !self.device.is_available() && self.hide_missing {
return Vec::new();
}
vec![&self.output]
}
fn id(&self) -> usize {
self.id
}
}