Implement system stats

This commit is contained in:
Moritz Ruth 2023-03-06 22:58:47 +01:00
parent d944132107
commit 0f4c1b80ab
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
8 changed files with 349 additions and 35 deletions

107
Cargo.lock generated
View file

@ -117,6 +117,23 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "battery"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b624268937c0e0a3edb7c27843f9e547c320d730c610d3b8e6e8e95b2026e4"
dependencies = [
"cfg-if",
"core-foundation 0.7.0",
"lazycell",
"libc",
"mach",
"nix 0.19.1",
"num-traits",
"uom",
"winapi",
]
[[package]]
name = "bit_field"
version = "0.10.2"
@ -195,16 +212,32 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
dependencies = [
"core-foundation-sys 0.7.0",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"core-foundation-sys 0.8.3",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@ -647,6 +680,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"battery",
"directories",
"env_logger",
"exitcode",
@ -661,10 +695,12 @@ dependencies = [
"rumqttc",
"serde",
"serde_json",
"sysinfo",
"tokio",
"toml",
"validator",
"void",
"zbus",
]
[[package]]
@ -826,6 +862,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lebe"
version = "0.5.2"
@ -886,6 +928,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -955,6 +1006,18 @@ dependencies = [
"getrandom",
]
[[package]]
name = "nix"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.23.2"
@ -1006,6 +1069,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "ntapi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
dependencies = [
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@ -1476,8 +1548,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"core-foundation 0.9.3",
"core-foundation-sys 0.8.3",
"libc",
"security-framework-sys",
]
@ -1488,7 +1560,7 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
dependencies = [
"core-foundation-sys",
"core-foundation-sys 0.8.3",
"libc",
]
@ -1647,6 +1719,20 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sysinfo"
version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e847e2de7a137c8c2cede5095872dbb00f4f9bf34d061347e36b43322acd56"
dependencies = [
"cfg-if",
"core-foundation-sys 0.8.3",
"libc",
"ntapi",
"once_cell",
"winapi",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.1.0"
@ -1768,6 +1854,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.42.0",
]
@ -1908,6 +1995,16 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "uom"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e76503e636584f1e10b9b3b9498538279561adcef5412927ba00c2b32c4ce5ed"
dependencies = [
"num-traits",
"typenum",
]
[[package]]
name = "url"
version = "2.3.1"
@ -2251,6 +2348,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"lazy_static",
"nix 0.25.1",
"once_cell",
"ordered-stream",
@ -2259,6 +2357,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"winapi",

View file

@ -11,6 +11,7 @@ dry_run = [] # will prevent some actions like shutting down
[dependencies]
anyhow = "1.0.69"
base64 = "0.21.0"
battery = "0.7.8"
directories = "4.0.1"
env_logger = "0.10.0"
exitcode = "1.1.2"
@ -25,7 +26,9 @@ regex = "1.7.1"
rumqttc = "0.20.0"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
sysinfo = { version = "0.28.2", default-features = false }
tokio = { version = "1.25.0", features = ["full"] }
toml = "0.7.2"
validator = { version = "0.16.0", features = ["derive"] }
void = "1.0.2"
zbus = { version = "3.10.0", default-features = false, features = ["tokio"] }

View file

@ -6,10 +6,10 @@
- [x] Command buttons
- [x] Notifications
- [x] Actions
- [ ] System stats
- [ ] CPU usage
- [ ] RAM usage
- [ ] Battery: level, charging status
- [x] System stats
- [x] CPU usage
- [x] RAM usage
- [x] Battery: level, charging status
- [ ] Media session
- [ ] PipeWire
- [ ] File watcher
@ -17,7 +17,7 @@
Ideas:
- Camera video stream
- Idle time
- Idle time (→ libseat)
## License

View file

@ -35,15 +35,6 @@ pub struct Internal {
pub stable_id: String,
}
#[derive(Serialize, Deserialize, Validate)]
pub struct Modules {
#[validate]
pub buttons: Option<modules::buttons::Config>,
#[validate]
pub notifications: Option<modules::notifications::Config>,
}
#[derive(Serialize, Deserialize, Validate)]
pub struct Config {
#[validate(custom = "validate_unique_id")]
@ -57,7 +48,7 @@ pub struct Config {
pub mqtt: Mqtt,
#[validate]
pub modules: Modules,
pub modules: modules::Config,
#[serde(rename = "DO_NOT_CHANGE")]
#[validate]
@ -85,13 +76,7 @@ fn create_example_config() -> Config {
internal: Internal {
stable_id: generate_alphanumeric_id(12),
},
modules: Modules {
buttons: Some(modules::buttons::Config {
enabled: false,
buttons: Vec::new(),
}),
notifications: Some(modules::notifications::Config { enabled: false }),
},
modules: modules::Config::default(),
}
}

View file

@ -53,13 +53,12 @@ async fn init_command_button(context: &mut InitializationContext, config: Button
let command_topic = context.full.mqtt.get_homeassistant_topic("button", &entity_id, "trigger");
context
.send_retained_mqtt_message(
context.full.mqtt.get_homeassistant_topic("button", &entity_id, "config"),
.publish_to_owned_mqtt_topic(
&context.full.mqtt.get_homeassistant_topic("button", &entity_id, "config"),
json::stringify(json::object! {
"availability_topic": context.full.mqtt.availability_topic.clone(),
"command_topic": command_topic.as_str(),
"device": context.full.mqtt.discovery_device_object.clone(),
"icon": "mdi:power",
"name": config.name,
"payload_press": BUTTON_TRIGGER_TEXT,
"object_id": entity_id.as_str(),

197
src/modules/info.rs Normal file
View file

@ -0,0 +1,197 @@
use std::fs;
use std::time::Duration;
use anyhow::{anyhow, Result};
use rumqttc::QoS;
use serde::{Deserialize, Serialize};
use sysinfo::{CpuExt, System, SystemExt};
use tokio::time::MissedTickBehavior;
use validator::Validate;
use crate::modules::InitializationContext;
const MODULE_ID: &str = "info";
#[derive(Serialize, Deserialize, Validate)]
pub struct Config {
#[serde(default)]
enabled: bool,
#[serde(default)]
ram_usage: u64,
#[serde(default)]
cpu_usage: u64,
#[serde(default)]
battery: u64,
}
pub async fn init(context: &mut InitializationContext) -> Result<()> {
let full_context = context.get_full();
let config = match &full_context.config.modules.info {
Some(c) if c.enabled => c,
_ => return Ok(()),
};
log::info!("Initializing…");
{
let mut sys = System::new();
init_info_value(
InfoEntityOptions {
sub_id: "ram_usage",
display_name: "RAM Usage",
icon: "mdi:memory",
suggested_precision: 0,
unit: Some("%"),
},
context,
config.ram_usage,
move || {
sys.refresh_memory();
Ok(format!("{:.2}", (sys.available_memory() as f64) / (sys.total_memory() as f64) * 100.0))
},
)
.await?;
}
{
let mut sys = System::new();
init_info_value(
InfoEntityOptions {
sub_id: "cpu_usage",
display_name: "CPU Usage",
icon: "mdi:memory",
suggested_precision: 0,
unit: Some("%"),
},
context,
config.cpu_usage,
move || {
sys.refresh_cpu();
Ok(format!("{:.2}", sys.global_cpu_info().cpu_usage()))
},
)
.await?;
}
if config.battery != 0 {
let battery_dirs = fs::read_dir("/sys/class/power_supply")?.filter_map(|d| d.ok()).collect::<Vec<_>>();
if let Some(dir) = battery_dirs.first() {
log::debug!("Found {} batteries, using {}", battery_dirs.len(), dir.file_name().to_string_lossy());
let path = dir.path();
let capacity_path = path.clone().join("capacity");
let status_path = path.clone().join("status");
init_info_value(
InfoEntityOptions {
sub_id: "battery_level",
display_name: "Battery Level",
icon: "mdi:battery",
suggested_precision: 0,
unit: Some("%"),
},
context,
config.battery,
move || Ok(fs::read_to_string(&capacity_path)?),
)
.await?;
init_info_value(
InfoEntityOptions {
sub_id: "battery_state",
display_name: "Battery State",
icon: "mdi:battery",
suggested_precision: 0,
unit: None,
},
context,
config.battery,
move || Ok(fs::read_to_string(&status_path)?),
)
.await?;
} else {
log::warn!("No batteries found.")
}
}
Ok(())
}
struct InfoEntityOptions<'a> {
sub_id: &'a str,
display_name: &'a str,
icon: &'a str,
suggested_precision: u64,
unit: Option<&'a str>,
}
async fn init_info_value(
options: InfoEntityOptions<'_>,
context: &mut InitializationContext,
interval_secs: u64,
mut fetch: impl (FnMut() -> Result<String>) + Send + 'static,
) -> Result<()> {
let interval = if interval_secs == 0 {
return Ok(());
} else {
Duration::from_secs(interval_secs)
};
let entity_id = context.full.get_entity_id(MODULE_ID, options.sub_id);
let state_topic = context.full.mqtt.get_homeassistant_topic("sensor", &entity_id, "state");
context.register_owned_mqtt_topic(&state_topic);
context
.publish_to_owned_mqtt_topic(
&context.full.mqtt.get_homeassistant_topic("sensor", &entity_id, "config"),
json::stringify({
let mut object = json::object! {
"availability_topic": context.full.mqtt.availability_topic.clone(),
"device": context.full.mqtt.discovery_device_object.clone(),
"force_update": true,
"icon": options.icon,
"name": options.display_name,
"suggested_display_precision": options.suggested_precision,
"state_class": "measurement",
"state_topic": state_topic.as_str(),
"object_id": entity_id.as_str(),
"unique_id": entity_id.as_str()
};
if let Some(unit) = options.unit {
object.insert("unit_of_measurement", unit).unwrap();
}
object
}),
)
.await?;
let mqtt_client = context.full.mqtt.client.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(interval);
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
interval.tick().await;
let result = fetch();
match result {
Err(error) => log::error!("{:#}", anyhow!(error)),
Ok(value) => {
if let Err(error) = mqtt_client.publish(&state_topic, QoS::ExactlyOnce, true, value).await {
log::error!("{:#}", anyhow!(error));
};
}
}
}
});
Ok(())
}

View file

@ -4,9 +4,24 @@ use std::sync::Arc;
use anyhow::Result;
use json::JsonValue;
use rumqttc::{AsyncClient as MqttClient, ClientError, QoS};
use serde::{Deserialize, Serialize};
use validator::Validate;
pub mod buttons;
pub mod notifications;
mod buttons;
mod info;
mod notifications;
#[derive(Serialize, Deserialize, Validate, Default)]
pub struct Config {
#[validate]
pub buttons: Option<buttons::Config>,
#[validate]
pub info: Option<info::Config>,
#[validate]
pub notifications: Option<notifications::Config>,
}
type MqttMessageHandler = dyn Fn(&str) -> Result<()>;
@ -29,14 +44,17 @@ impl InitializationContext {
self.message_handler_by_mqtt_topic.insert(topic.into(), Box::new(handler));
}
async fn send_retained_mqtt_message(&mut self, topic: impl Into<String>, message: impl Into<String>) -> std::result::Result<(), ClientError> {
let topic = topic.into();
async fn publish_to_owned_mqtt_topic(&mut self, topic: &str, message: impl Into<String>) -> std::result::Result<(), ClientError> {
let message = message.into();
self.owned_mqtt_topics.insert(topic.to_owned());
self.register_owned_mqtt_topic(topic);
self.full.mqtt.client.publish(topic, QoS::AtLeastOnce, true, message).await
}
fn register_owned_mqtt_topic(&mut self, topic: &str) {
self.owned_mqtt_topics.insert(topic.into());
}
fn get_full(&self) -> Arc<ModuleContext> {
self.full.clone()
}
@ -61,6 +79,7 @@ impl ModuleContextMqtt {
pub async fn init_all(context: &mut InitializationContext) -> Result<()> {
buttons::init(context).await?;
info::init(context).await?;
notifications::init(context).await?;
Ok(())
}

View file

@ -11,11 +11,23 @@ use tokio::task::JoinHandle;
pub fn spawn_nonessential(future: impl Future<Output=Result<()>> + Send + 'static) -> JoinHandle<()> {
tokio::spawn(async {
if let Err(error) = future.await {
log::error!("{:#}", error)
log_error(error);
}
})
}
#[inline]
pub fn run_nonessential(block: impl FnOnce() -> Result<()>) {
if let Err(error) = block() {
log_error(error);
}
}
#[inline]
pub fn log_error(error: anyhow::Error) {
log::error!("{:#}", error);
}
pub fn generate_alphanumeric_id(length: usize) -> String {
rand::thread_rng().sample_iter(&Alphanumeric).take(length).map(char::from).collect()
}