diff --git a/README.md b/README.md index afb4293..6aa0b69 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Use e.g. `1` for stable v1.y.z releases and `latest` for bleeding/dev/unstable r - `HTTP_PORT` (defaults to `9995`): The HTTP server port. - `HTTP_PATH` (defaults to `nut`): The HTTP server metrics path. You may want to set it to `/metrics` on new setups to avoid extra Prometheus configuration (not changed here due to compatibility). - `PRINT_METRICS_AND_EXIT` (defaults to `false`): Print a Markdown-formatted table consisting of all metrics and then immediately exit. Used mainly for generating documentation. +- `UPS_POWER_FROM_LOAD_PERCENTAGE` (defaults to `false`): when set to `true` and a value for `ups.power` is not available from a UPS, an approximate value will be calculated for the metric `nut_power_watts` (Apparent Power). Useful for UPS units that do not report the load in Watts, like some CyberPower units. The calculation will be: `( ups.load / 100.00 ) * ups.realpower.nominal`. ## Metrics diff --git a/src/config.rs b/src/config.rs index c0b706f..3688a59 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ pub struct Config { pub http_port: u16, pub http_path: String, pub print_metrics_and_exit: bool, + pub ups_power_from_load_percentage: bool, } impl Config { @@ -16,6 +17,7 @@ impl Config { const DEFAULT_HTTP_PORT: u16 = 9995; const DEFAULT_HTTP_PATH: &'static str = "/nut"; const DEFAULT_PRINT_METRICS_AND_EXIT: bool = false; + const DEFAULT_UPS_POWER_FROM_LOAD_PERCENTAGE: bool = false; } pub fn read_config() -> Config { @@ -24,6 +26,7 @@ pub fn read_config() -> Config { http_port: Config::DEFAULT_HTTP_PORT, http_path: Config::DEFAULT_HTTP_PATH.to_owned(), print_metrics_and_exit: Config::DEFAULT_PRINT_METRICS_AND_EXIT, + ups_power_from_load_percentage: Config::DEFAULT_UPS_POWER_FROM_LOAD_PERCENTAGE, }; if let Ok(http_address_str) = std::env::var("HTTP_ADDRESS") { @@ -31,7 +34,6 @@ pub fn read_config() -> Config { config.http_address = http_address; } } - if let Ok(http_port_str) = std::env::var("HTTP_PORT") { if let Ok(http_port) = http_port_str.parse::() { config.http_port = http_port; @@ -47,6 +49,11 @@ pub fn read_config() -> Config { config.print_metrics_and_exit = print_metrics_and_exit; } } + if let Ok(ups_power_from_load_percentage_str) = std::env::var("UPS_POWER_FROM_LOAD_PERCENTAGE") { + if let Ok(ups_power_from_load_percentage) = ups_power_from_load_percentage_str.parse::() { + config.ups_power_from_load_percentage = ups_power_from_load_percentage; + } + } config } diff --git a/src/nut_client.rs b/src/nut_client.rs index 124bc13..7ea08c1 100644 --- a/src/nut_client.rs +++ b/src/nut_client.rs @@ -6,6 +6,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; use crate::common::ErrorResult; +use crate::config; use crate::metrics::{NutVersion, UPS_DESCRIPTION_PSEUDOVAR, UpsVarMap, VarMap}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -99,28 +100,52 @@ async fn query_nut_vars(stream: &mut BufReader, upses: &mut UpsVarMap } for (ups, vars) in upses.iter_mut() { - let line_consumer = |line: &str| { - let captures_opt = VAR_PATTERN.captures(line); - match captures_opt { - Some(captures) => { - let variable = captures["var"].to_owned(); - let value = captures["val"].to_owned(); - vars.insert(variable, value); - }, - None => { - return Err("Malformed list element for VAR list query.".into()); - }, - } - - Ok(()) - }; + { + let line_consumer = |line: &str| { + let captures_opt = VAR_PATTERN.captures(line); + match captures_opt { + Some(captures) => { + let variable = captures["var"].to_owned(); + let value = captures["val"].to_owned(); + vars.insert(variable, value); + }, + None => { + return Err("Malformed list element for VAR list query.".into()); + }, + } + + Ok(()) + }; + + query_nut_list(stream, format!("LIST VAR {}", ups).as_str(), line_consumer).await?; + } - query_nut_list(stream, format!("LIST VAR {}", ups).as_str(), line_consumer).await?; + override_nut_values(vars).await; } Ok(()) } + +async fn override_nut_values(vars: &mut HashMap) { + let config = config::read_config(); + + // For UPS systems where ups.power is not available, but ups.load and ups.realpower.nominal are. + // Provide a calculated value for ups.power with the following algorithm: + // power (W) = ( load(%) / 100.00 ) * nominal power (W) + if config.ups_power_from_load_percentage && !( vars.contains_key("ups.power") ) && vars.contains_key("ups.load") && vars.contains_key("ups.realpower.nominal") { + if let (Ok(load), Ok(nominal_power)) = ( + vars.get("ups.load").unwrap().parse::(), + vars.get("ups.realpower.nominal").unwrap().parse::(), + ) { + let calculated_power = (load / 100.0) * nominal_power; + vars.insert(String::from("ups.power"), calculated_power.to_string()); + } else { + log::error!("Unable to parse ups.load or ups.realpower.nominal as floats, skipping calculation of ups.power from these values"); + } + } +} + async fn query_nut_list(stream: &mut BufReader, query: &str, mut line_consumer: F) -> ErrorResult<()> where F: FnMut(&str) -> ErrorResult<()> + Send { let query_line = format!("{}\n", query);