Implement system stats
This commit is contained in:
parent
d944132107
commit
0f4c1b80ab
8 changed files with 349 additions and 35 deletions
107
Cargo.lock
generated
107
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
10
README.md
10
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
197
src/modules/info.rs
Normal 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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
14
src/util.rs
14
src/util.rs
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue