diff --git a/Cargo.toml b/Cargo.toml index bd1f625..64a44a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nut-client" -version = "0.0.2" +version = "0.0.3" authors = ["Aram Peres "] edition = "2018" description = "Network UPS Tools (NUT) client library" diff --git a/README.md b/README.md index 65320be..93912fd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A [Network UPS Tools](https://github.com/networkupstools/nut) (NUT) client libra - Connect to `upsd`/`nut-server` using TCP - Login with with username and password - List UPS devices +- List variables for a UPS device ## ⚠️ Safety Goggles Required ⚠️ @@ -26,11 +27,10 @@ Check out the `examples` directory for more advanced examples. use std::env; use std::net::ToSocketAddrs; -use nut_client::{Auth, ConfigBuilder, Host}; use nut_client::blocking::Connection; +use nut_client::{Auth, ConfigBuilder, Host}; fn main() -> nut_client::Result<()> { - // The TCP host:port for upsd/nut-server let addr = env::var("NUT_ADDR") .unwrap_or_else(|_| "localhost:3493".into()) .to_socket_addrs() @@ -38,25 +38,27 @@ fn main() -> nut_client::Result<()> { .next() .unwrap(); - // Username and password (optional) let username = env::var("NUT_USER").ok(); let password = env::var("NUT_PASSWORD").ok(); let auth = username.map(|username| Auth::new(username, password)); - // Build the config let config = ConfigBuilder::new() .with_host(Host::Tcp(addr)) .with_auth(auth) .build(); - // Open a connection and login let mut conn = Connection::new(config)?; - // Print a list of all UPS devices + // Print a list of all UPS devices and their variables println!("Connected UPS devices:"); - for (id, description) in conn.list_ups()? { - println!("\t- ID: {}", id); + for (name, description) in conn.list_ups()? { + println!("\t- Name: {}", name); println!("\t Description: {}", description); + println!("\t Variables:"); + + for (var_name, var_val) in conn.list_vars(&name)? { + println!("\t\t- {} = {}", var_name, var_val); + } } Ok(()) diff --git a/examples/blocking.rs b/examples/blocking.rs index 3fc9231..1b518d8 100644 --- a/examples/blocking.rs +++ b/examples/blocking.rs @@ -23,11 +23,16 @@ fn main() -> nut_client::Result<()> { let mut conn = Connection::new(config)?; - // Print a list of all UPS devices + // Print a list of all UPS devices and their variables println!("Connected UPS devices:"); - for (id, description) in conn.list_ups()? { - println!("\t- ID: {}", id); + for (name, description) in conn.list_ups()? { + println!("\t- Name: {}", name); println!("\t Description: {}", description); + println!("\t Variables:"); + + for (var_name, var_val) in conn.list_vars(&name)? { + println!("\t\t- {} = {}", var_name, var_val); + } } Ok(()) diff --git a/src/blocking/mod.rs b/src/blocking/mod.rs index 882df8c..46dc01f 100644 --- a/src/blocking/mod.rs +++ b/src/blocking/mod.rs @@ -7,10 +7,12 @@ use crate::{ClientError, Config, Host, NutError}; /// A blocking NUT client connection. pub enum Connection { + /// A TCP connection. Tcp(TcpConnection), } impl Connection { + /// Initializes a connection to a NUT server (upsd). pub fn new(config: Config) -> crate::Result { match &config.host { Host::Tcp(socket_addr) => { @@ -19,11 +21,19 @@ impl Connection { } } + /// Queries a list of UPS devices. pub fn list_ups(&mut self) -> crate::Result> { match self { Self::Tcp(conn) => conn.list_ups(), } } + + /// Queries the list of variables for a UPS device. + pub fn list_vars(&mut self, ups_name: &str) -> crate::Result> { + match self { + Self::Tcp(conn) => conn.list_vars(ups_name), + } + } } /// A blocking TCP NUT client connection. @@ -70,6 +80,17 @@ impl TcpConnection { .collect()) } + fn list_vars(&mut self, ups_name: &str) -> crate::Result> { + let query = &["VAR", ups_name]; + Self::write_cmd(&mut self.tcp_stream, Command::List(query))?; + let list = Self::read_list(&mut self.tcp_stream, query)?; + + Ok(list + .into_iter() + .map(|mut row| (row.remove(0), row.remove(0))) + .collect()) + } + fn write_cmd(stream: &mut TcpStream, line: Command) -> crate::Result<()> { let line = format!("{}\n", line); stream.write_all(line.as_bytes())?; diff --git a/src/cmd.rs b/src/cmd.rs index 4329d24..0189d00 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -67,6 +67,7 @@ impl Response { let err_type = args.remove(0); match err_type.as_str() { "ACCESS-DENIED" => Err(NutError::AccessDenied.into()), + "UNKNOWN-UPS" => Err(NutError::UnknownUps.into()), _ => Err(NutError::Generic(format!( "Server error: {} {}", err_type, diff --git a/src/config.rs b/src/config.rs index e18fc7b..b21cf29 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,7 @@ pub struct Auth { } impl Auth { + /// Initializes authentication credentials with a username, and optionally a password. pub fn new(username: String, password: Option) -> Self { Auth { username, password } } @@ -54,6 +55,7 @@ pub struct Config { } impl Config { + /// Creates a connection configuration. pub fn new(host: Host, auth: Option, timeout: Duration) -> Self { Config { host, @@ -77,21 +79,26 @@ impl ConfigBuilder { ConfigBuilder::default() } + /// Sets the connection host, such as the TCP address and port. pub fn with_host(mut self, host: Host) -> Self { self.host = Some(host); self } + /// Sets the optional authentication parameters. pub fn with_auth(mut self, auth: Option) -> Self { self.auth = auth; self } + /// Sets the network connection timeout. This may be ignored by non-network + /// connections, such as Unix domain sockets. pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } + /// Builds the configuration with this builder. pub fn build(self) -> Config { Config::new( self.host.unwrap_or_default(), diff --git a/src/error.rs b/src/error.rs index 53da74f..1f83939 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,7 +6,11 @@ use std::io; pub enum NutError { /// Occurs when the username/password combination is rejected. AccessDenied, + /// Occurs when the specified UPS device does not exist. + UnknownUps, + /// Occurs when the response type or content wasn't expected at the current stage. UnexpectedResponse, + /// Occurs when the response type is not recognized by the client. UnknownResponseType(String), /// Generic (usually internal) client error. Generic(String), @@ -16,7 +20,8 @@ impl fmt::Display for NutError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::AccessDenied => write!(f, "Authentication failed"), - Self::UnexpectedResponse => write!(f, "Unexpected server response"), + Self::UnknownUps => write!(f, "Unknown UPS device name"), + Self::UnexpectedResponse => write!(f, "Unexpected server response content"), Self::UnknownResponseType(ty) => write!(f, "Unknown response type: {}", ty), Self::Generic(msg) => write!(f, "Internal client error: {}", msg), } @@ -25,9 +30,12 @@ impl fmt::Display for NutError { impl std::error::Error for NutError {} +/// Encapsulation for errors emitted by the client library. #[derive(Debug)] pub enum ClientError { + /// Encapsulates IO errors. Io(io::Error), + /// Encapsulates NUT and client-specific errors. Nut(NutError), } @@ -54,4 +62,5 @@ impl From for ClientError { } } +/// Result type for [`ClientError`] pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 34ec24d..482462a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,14 @@ +#![deny(missing_docs)] + +//! # nut-client +//! +//! The `nut-client` crate provides a network client implementation +//! for Network UPS Tools (NUT) servers. + pub use config::*; pub use error::*; +/// Blocking client implementation for NUT. pub mod blocking; mod cmd;