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",
]
[[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]]
name = "num-traits"
version = "0.2.17"
@ -1032,6 +1043,8 @@ dependencies = [
"local-ip-address",
"log",
"maxminddb",
"num-derive",
"num-traits",
"pretty_env_logger",
"reqwest",
"serde",

View file

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

View file

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

View file

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

View file

@ -226,7 +226,7 @@ pub fn launch_tile(
) -> JoinHandle<()> {
let output_chan = OutputChannel::with_random_uuid(sender);
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) => {
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 (󰂅)
];
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;
if charging {
BATTERY_CHARGING_SYMBOLS[index]

View file

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

View file

@ -1,57 +1,156 @@
#![allow(clippy::option_map_unit_fn)]
use crate::config::BatteryConfig;
use crate::formatting::charging_symbol;
use crate::formatting::battery_symbol;
use crate::generated::OrgFreedesktopUPowerDevice;
use crate::output::OutputChannel;
use crate::tile::Block;
use std::convert::Infallible;
use std::path::Path;
use std::str::FromStr;
use tokio::fs::try_exists;
use dbus::arg::RefArg;
use dbus::arg::Variant;
use dbus::message::SignalArgs;
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;
pub async fn battery(config: BatteryConfig, output: OutputChannel) -> eyre::Result<Infallible> {
let base_dir = Path::new("/sys/class/power_supply").join(&*config.battery);
let file_prefix = if try_exists(base_dir.join("energy_now")).await? {
"energy_"
} else {
"charge_"
};
let now_path = base_dir.join(file_prefix.to_owned() + "now");
let full_path = base_dir.join(file_prefix.to_owned() + "full");
let status_path = base_dir.join("status");
#[derive(FromPrimitive, Debug, PartialEq, Eq)]
enum DeviceState {
Unknown = 0,
Charging = 1,
Discharging = 2,
Empty = 3,
FullyCharged = 4,
PendingCharge = 5,
}
let mut interval = tokio::time::interval(config.update);
loop {
interval.tick().await;
#[derive(Debug)]
struct BatteryInfo {
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>
where
<T as FromStr>::Err: Send + Sync + std::error::Error + 'static,
{
Ok(tokio::fs::read_to_string(path).await?.trim_end().parse()?)
fn format_duration(dur: Duration) -> String {
let hours = dur.as_secs() / 60 / 60;
let minutes = (dur.as_secs() / 60) % 60;
format!(" ({}:{:02})", hours, minutes)
}
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);
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)?;
eprintln!("{:#?}", init_info);
let percentage = charge_now * 100.0 / charge_total;
let is_charging = status != "Discharging";
let block = if config.use_symbols {
let symbol = charging_symbol(percentage, is_charging);
let (tx, mut rx) = watch::channel(init_info);
let _reciever = dbus_conn
.add_match(PropertiesPropertiesChanged::match_rule(
Some(dest),
Some(path),
))
.await?
.cb(move |_, changed: PropertiesPropertiesChanged| {
tx.send_modify(|info| {
let props = changed.changed_properties;
props
.get("Percentage")
.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, percentage).into(),
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}% {}", percentage, status).into(),
short_text: format!("{:.0}%", percentage).into_boxed_str().into(),
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?;
rx.changed().await?;
}
}