battery: first attempt using upower

This commit is contained in:
Artemis Tosini 2023-12-24 22:11:59 +00:00
parent 66a117dfcc
commit fb76f49325
Signed by: artemist
GPG key ID: ADFFE553DCBB831E
8 changed files with 160 additions and 44 deletions

13
Cargo.lock generated
View file

@ -712,6 +712,17 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "num-derive"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.17" version = "0.2.17"
@ -1032,6 +1043,8 @@ dependencies = [
"local-ip-address", "local-ip-address",
"log", "log",
"maxminddb", "maxminddb",
"num-derive",
"num-traits",
"pretty_env_logger", "pretty_env_logger",
"reqwest", "reqwest",
"serde", "serde",

View file

@ -17,6 +17,8 @@ futures = { version = "0.3", default-features = false }
local-ip-address = "0.5" local-ip-address = "0.5"
log = "0.4" log = "0.4"
maxminddb = "0.23" maxminddb = "0.23"
num-derive = "0.4"
num-traits = "0.2"
pretty_env_logger = "0.5" pretty_env_logger = "0.5"
reqwest = { version = "0.11", features = ["rustls-tls"], default-features = false } reqwest = { version = "0.11", features = ["rustls-tls"], default-features = false }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -16,6 +16,7 @@ fn main() {
"org.freedesktop.NetworkManager.AccessPoint.xml", "org.freedesktop.NetworkManager.AccessPoint.xml",
"org.freedesktop.NetworkManager.Connection.Active.xml", "org.freedesktop.NetworkManager.Connection.Active.xml",
"org.freedesktop.NetworkManager.xml", "org.freedesktop.NetworkManager.xml",
"org.freedesktop.UPower.Device.xml",
"org.freedesktop.hostname1.xml", "org.freedesktop.hostname1.xml",
]); ]);
let required_interfaces = required_files let required_interfaces = required_files

View file

@ -13,7 +13,7 @@
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
inherit (pkgs) lib; inherit (pkgs) lib;
dbusPaths = lib.makeSearchPathOutput "out" "share/dbus-1/interfaces" dbusPaths = lib.makeSearchPathOutput "out" "share/dbus-1/interfaces"
(with pkgs; [ systemd networkmanager ]); (with pkgs; [ systemd networkmanager upower ]);
in rec { in rec {
packages.rustybar = with pkgs; packages.rustybar = with pkgs;
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage {

View file

@ -226,7 +226,7 @@ pub fn launch_tile(
) -> JoinHandle<()> { ) -> JoinHandle<()> {
let output_chan = OutputChannel::with_random_uuid(sender); let output_chan = OutputChannel::with_random_uuid(sender);
match tile.clone() { match tile.clone() {
TileConfig::Battery(c) => spawn(tiles::battery(c, output_chan), tile_id), TileConfig::Battery(c) => spawn(tiles::battery(c, output_chan, dbus_conn.clone()), tile_id),
TileConfig::Hostname(c) => { TileConfig::Hostname(c) => {
spawn(tiles::hostname(c, output_chan, dbus_conn.clone()), tile_id) spawn(tiles::hostname(c, output_chan, dbus_conn.clone()), tile_id)
} }

View file

@ -26,7 +26,7 @@ static BATTERY_CHARGING_SYMBOLS: &[char] = &[
'\u{f0085}', // nf-md-battery_charging_100 (󰂅) '\u{f0085}', // nf-md-battery_charging_100 (󰂅)
]; ];
pub fn charging_symbol(percentage: f64, charging: bool) -> char { pub fn battery_symbol(percentage: f64, charging: bool) -> char {
let index = (percentage.clamp(0.0, 100.0) / 10.0).round() as usize; let index = (percentage.clamp(0.0, 100.0) / 10.0).round() as usize;
if charging { if charging {
BATTERY_CHARGING_SYMBOLS[index] BATTERY_CHARGING_SYMBOLS[index]

View file

@ -22,6 +22,7 @@ async fn main() -> eyre::Result<()> {
let err = resource.await; let err = resource.await;
panic!("Lost connection to D-Bus: {}", err); panic!("Lost connection to D-Bus: {}", err);
}); });
dbus_conn.set_signal_match_mode(true);
let mut stream_map = StreamMap::new(); let mut stream_map = StreamMap::new();

View file

@ -1,57 +1,156 @@
#![allow(clippy::option_map_unit_fn)]
use crate::config::BatteryConfig; use crate::config::BatteryConfig;
use crate::formatting::charging_symbol; use crate::formatting::battery_symbol;
use crate::generated::OrgFreedesktopUPowerDevice;
use crate::output::OutputChannel; use crate::output::OutputChannel;
use crate::tile::Block; use crate::tile::Block;
use std::convert::Infallible; use dbus::arg::RefArg;
use std::path::Path; use dbus::arg::Variant;
use std::str::FromStr; use dbus::message::SignalArgs;
use tokio::fs::try_exists; use dbus::nonblock::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged;
use dbus::nonblock::{Proxy, SyncConnection};
use dbus::strings::BusName;
use eyre::OptionExt;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use std::convert::TryInto;
use std::sync::Arc;
use std::{convert::Infallible, time::Duration};
use tokio::sync::watch;
use tokio::try_join; use tokio::try_join;
pub async fn battery(config: BatteryConfig, output: OutputChannel) -> eyre::Result<Infallible> { #[derive(FromPrimitive, Debug, PartialEq, Eq)]
let base_dir = Path::new("/sys/class/power_supply").join(&*config.battery); enum DeviceState {
let file_prefix = if try_exists(base_dir.join("energy_now")).await? { Unknown = 0,
"energy_" Charging = 1,
} else { Discharging = 2,
"charge_" Empty = 3,
}; FullyCharged = 4,
let now_path = base_dir.join(file_prefix.to_owned() + "now"); PendingCharge = 5,
let full_path = base_dir.join(file_prefix.to_owned() + "full"); }
let status_path = base_dir.join("status");
let mut interval = tokio::time::interval(config.update); #[derive(Debug)]
loop { struct BatteryInfo {
interval.tick().await; pub percentage: f64,
pub state: DeviceState,
pub time_to_empty: Duration,
pub time_to_full: Duration,
}
async fn read_and_parse<T: FromStr + Sync>(path: &Path) -> eyre::Result<T> fn format_duration(dur: Duration) -> String {
where let hours = dur.as_secs() / 60 / 60;
<T as FromStr>::Err: Send + Sync + std::error::Error + 'static, let minutes = (dur.as_secs() / 60) % 60;
{ format!(" ({}:{:02})", hours, minutes)
Ok(tokio::fs::read_to_string(path).await?.trim_end().parse()?) }
pub async fn battery(
config: BatteryConfig,
output: OutputChannel,
dbus_conn: Arc<SyncConnection>,
) -> eyre::Result<Infallible> {
let dest = Box::leak(Box::new(BusName::new("org.freedesktop.UPower").unwrap()));
let path = Box::leak(Box::new(
dbus::Path::new("/org/freedesktop/UPower/devices/DisplayDevice").unwrap(),
));
let proxy = Proxy::new(
dest.clone(),
path.clone(),
Duration::from_secs(5),
dbus_conn.as_ref(),
);
let init_info = {
let (percentage, raw_state, time_to_empty, time_to_full) = try_join!(
proxy.percentage(),
proxy.state(),
proxy.time_to_empty(),
proxy.time_to_full()
)?;
BatteryInfo {
percentage,
state: DeviceState::from_u32(raw_state).ok_or_eyre("Invalid state")?,
time_to_empty: Duration::from_secs(time_to_empty.clamp(0, i64::MAX).try_into()?),
time_to_full: Duration::from_secs(time_to_full.clamp(0, i64::MAX).try_into()?),
} }
};
let charge_now = read_and_parse::<f64>(&now_path); eprintln!("{:#?}", init_info);
let charge_total = read_and_parse::<f64>(&full_path);
let status = read_and_parse::<String>(&status_path);
let (charge_now, charge_total, status) = try_join!(charge_now, charge_total, status)?;
let percentage = charge_now * 100.0 / charge_total; let (tx, mut rx) = watch::channel(init_info);
let is_charging = status != "Discharging";
let block = if config.use_symbols { let _reciever = dbus_conn
let symbol = charging_symbol(percentage, is_charging); .add_match(PropertiesPropertiesChanged::match_rule(
Block { Some(dest),
full_text: format!("{} {:.0}%", symbol, percentage).into(), Some(path),
short_text: symbol.to_string().into_boxed_str().into(), ))
..Default::default() .await?
} .cb(move |_, changed: PropertiesPropertiesChanged| {
} else { tx.send_modify(|info| {
Block { let props = changed.changed_properties;
full_text: format!("{:.0}% {}", percentage, status).into(), props
short_text: format!("{:.0}%", percentage).into_boxed_str().into(), .get("Percentage")
..Default::default() .and_then(Variant::as_f64)
.map(|val| info.percentage = val);
props
.get("State")
.and_then(Variant::as_u64)
.and_then(DeviceState::from_u64)
.map(|val| info.state = val);
props
.get("TimeToEmpty")
.and_then(Variant::as_i64)
.map(|val| val.clamp(0, i64::MAX).try_into().unwrap())
.map(Duration::from_secs)
.map(|val| info.time_to_empty = val);
props
.get("TimeToFull")
.and_then(Variant::as_i64)
.map(|val| val.clamp(0, i64::MAX).try_into().unwrap())
.map(Duration::from_secs)
.map(|val| info.time_to_full = val);
});
true
});
loop {
let block = {
let info = rx.borrow();
let time_str = match info.state {
DeviceState::Discharging => format_duration(info.time_to_empty),
DeviceState::Charging => format_duration(info.time_to_full),
_ => "".to_string(),
};
if config.use_symbols {
let symbol = match info.state {
DeviceState::Charging | DeviceState::FullyCharged => {
battery_symbol(info.percentage, true)
}
DeviceState::Discharging | DeviceState::Empty => {
battery_symbol(info.percentage, false)
}
DeviceState::Unknown => '\u{f0091}',
DeviceState::PendingCharge => '\u{f1211}',
};
Block {
full_text: format!("{} {:.0}%{}", symbol, info.percentage, time_str).into(),
short_text: symbol.to_string().into_boxed_str().into(),
..Default::default()
}
} else {
Block {
full_text: format!("{:.0}% {:?}{}", info.percentage, info.state, time_str)
.into(),
short_text: format!("{:.0}%", info.percentage).into_boxed_str().into(),
..Default::default()
}
} }
}; };
output.send(block).await?; output.send(block).await?;
rx.changed().await?;
} }
} }