1use std::fmt;
2use std::time::Duration;
3
4use reqwest::{Client, Method};
5use serde_json::Value;
6
7use crate::error::{Result, UnifiError};
8use crate::{http, UnifiConfig};
9
10#[derive(Clone)]
23pub struct UnifiClient {
24 http: Client,
25 pub url: String,
27 api_key: String,
28 site: String,
29 skip_tls_verify: bool,
30 legacy: bool,
31 request_timeout: Duration,
32}
33
34impl fmt::Debug for UnifiClient {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.debug_struct("UnifiClient")
37 .field("url", &self.url)
38 .field(
39 "api_key",
40 &format_args!("<redacted, {} bytes>", self.api_key.len()),
41 )
42 .field("site", &self.site)
43 .field("skip_tls_verify", &self.skip_tls_verify)
44 .field("legacy", &self.legacy)
45 .field("request_timeout", &self.request_timeout)
46 .finish()
47 }
48}
49
50impl UnifiClient {
51 pub fn new(cfg: &UnifiConfig) -> Result<Self> {
58 if cfg.url.is_empty() {
59 return Err(UnifiError::MissingUrl);
60 }
61 if cfg.api_key.is_empty() {
62 return Err(UnifiError::MissingApiKey);
63 }
64 let http = http::build_client(cfg)?;
65 Ok(Self {
66 http,
67 url: cfg.url.trim_end_matches('/').to_string(),
68 api_key: cfg.api_key.clone(),
69 site: cfg.site.clone(),
70 skip_tls_verify: cfg.skip_tls_verify,
71 legacy: cfg.legacy,
72 request_timeout: cfg.request_timeout,
73 })
74 }
75
76 pub fn site(&self) -> &str {
78 &self.site
79 }
80
81 pub fn legacy(&self) -> bool {
83 self.legacy
84 }
85
86 pub fn config(&self) -> UnifiConfig {
88 UnifiConfig {
89 url: self.url.clone(),
90 api_key: self.api_key.clone(),
91 site: self.site.clone(),
92 skip_tls_verify: self.skip_tls_verify,
93 legacy: self.legacy,
94 request_timeout: self.request_timeout,
95 }
96 }
97
98 pub async fn request_json(
109 &self,
110 action: &str,
111 method: Method,
112 path: &str,
113 query: Option<&Value>,
114 body: Option<&Value>,
115 ) -> Result<Value> {
116 http::request_json(
117 &self.http,
118 &self.url,
119 &self.api_key,
120 action,
121 method,
122 path,
123 query,
124 body,
125 )
126 .await
127 }
128
129 fn site_path(&self, suffix: &str) -> String {
130 let prefix = if self.legacy { "" } else { "/proxy/network" };
131 format!("{prefix}/api/s/{site}/{suffix}", site = self.site)
132 }
133
134 fn self_path(&self) -> &'static str {
135 if self.legacy {
136 "/api/self"
137 } else {
138 "/proxy/network/api/self"
139 }
140 }
141
142 async fn get(&self, path: &str, action: &str) -> Result<Value> {
143 self.request_json(action, Method::GET, path, None, None)
144 .await
145 }
146
147 pub async fn clients(&self) -> Result<Value> {
152 self.get(&self.site_path("stat/sta"), "clients").await
153 }
154
155 pub async fn devices(&self) -> Result<Value> {
160 self.get(&self.site_path("stat/device"), "devices").await
161 }
162
163 pub async fn wlans(&self) -> Result<Value> {
168 self.get(&self.site_path("rest/wlanconf"), "wlans").await
169 }
170
171 pub async fn health(&self) -> Result<Value> {
176 self.get(&self.site_path("stat/health"), "health").await
177 }
178
179 pub async fn alarms(&self) -> Result<Value> {
184 self.get(&self.site_path("rest/alarm"), "alarms").await
185 }
186
187 pub async fn events(&self) -> Result<Value> {
192 self.get(&self.site_path("rest/event"), "events").await
193 }
194
195 pub async fn sysinfo(&self) -> Result<Value> {
200 self.get(&self.site_path("stat/sysinfo"), "sysinfo").await
201 }
202
203 pub async fn me(&self) -> Result<Value> {
208 self.get(self.self_path(), "me").await
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn config_round_trips_a_non_default_request_timeout() {
218 let cfg = UnifiConfig {
219 url: "https://unifi.local".to_string(),
220 api_key: "test-key".to_string(),
221 request_timeout: Duration::from_secs(90),
222 ..UnifiConfig::default()
223 };
224
225 let client = UnifiClient::new(&cfg).unwrap();
226
227 assert_eq!(client.config().request_timeout, Duration::from_secs(90));
228 }
229}