1use serde::Deserialize;
2use std::fmt;
3
4#[cfg(test)]
5#[path = "tests.rs"]
6mod tests;
7
8#[derive(Debug, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct PaginatedResponse<T> {
12 pub total_count: usize,
13 pub data: Vec<T>,
14}
15
16#[derive(Debug, Deserialize)]
18pub struct LegacyResponse<T> {
19 pub meta: LegacyMeta,
20 pub data: Vec<T>,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct LegacyMeta {
25 pub rc: String,
26 pub msg: Option<String>,
27}
28
29#[derive(Debug, Deserialize)]
31pub struct Site {
32 pub id: String,
33}
34
35#[derive(Debug, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct Client {
39 #[serde(alias = "macAddress")]
40 pub mac_address: Option<String>,
41 #[serde(alias = "ipAddress")]
42 pub ip_address: Option<String>,
43 pub name: Option<String>,
44 pub hostname: Option<String>,
45 #[serde(alias = "type")]
46 pub client_type: Option<String>,
47}
48
49impl Client {
50 pub fn display_name(&self) -> &str {
51 self.name
52 .as_deref()
53 .or(self.hostname.as_deref())
54 .unwrap_or("-")
55 }
56}
57
58#[derive(Debug, Deserialize)]
60pub struct LegacyClient {
61 #[serde(rename = "_id")]
62 pub id: String,
63 pub mac: Option<String>,
64 pub ip: Option<String>,
65 pub hostname: Option<String>,
66 pub name: Option<String>,
67 #[serde(default)]
68 pub is_wired: bool,
69 pub uptime: Option<u64>,
70 pub tx_bytes: Option<u64>,
71 pub rx_bytes: Option<u64>,
72 pub signal: Option<i32>,
73 pub ap_mac: Option<String>,
74 #[serde(rename = "essid")]
75 pub ssid: Option<String>,
76}
77
78impl LegacyClient {
79 pub fn display_name(&self) -> &str {
80 self.name
81 .as_deref()
82 .or(self.hostname.as_deref())
83 .unwrap_or("-")
84 }
85}
86
87#[derive(Debug, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct Device {
91 pub mac_address: Option<String>,
92 pub ip_address: Option<String>,
93 pub name: Option<String>,
94 pub model: Option<String>,
95 pub state: Option<String>,
96 pub firmware_version: Option<String>,
97}
98
99#[derive(Debug, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Network {
103 pub name: Option<String>,
104 #[serde(default)]
105 pub enabled: bool,
106 pub vlan_id: Option<u16>,
107 #[serde(default)]
108 pub default: bool,
109}
110
111#[derive(Debug, Deserialize)]
113pub struct HealthSubsystem {
114 pub subsystem: String,
115 pub status: Option<String>,
116 pub num_sta: Option<u32>,
117 pub num_ap: Option<u32>,
118 #[serde(rename = "num_sw")]
119 pub num_switches: Option<u32>,
120 pub wan_ip: Option<String>,
121 pub isp_name: Option<String>,
122}
123
124#[derive(Debug, Deserialize)]
126pub struct SysInfo {
127 pub hostname: Option<String>,
128 pub version: Option<String>,
129 pub timezone: Option<String>,
130 pub uptime: Option<u64>,
131}
132
133pub fn normalize_mac(mac: &str) -> String {
134 mac.to_lowercase().replace([':', '-'], "")
135}
136
137pub fn format_mac(mac: &str) -> String {
138 let clean = normalize_mac(mac);
139 if clean.len() != 12 {
140 return mac.to_string();
141 }
142 format!(
143 "{}:{}:{}:{}:{}:{}",
144 &clean[0..2],
145 &clean[2..4],
146 &clean[4..6],
147 &clean[6..8],
148 &clean[8..10],
149 &clean[10..12]
150 )
151}
152
153pub fn format_bytes(bytes: u64) -> String {
154 const KB: u64 = 1024;
155 const MB: u64 = KB * 1024;
156 const GB: u64 = MB * 1024;
157
158 if bytes >= GB {
159 format!("{:.1} GB", bytes as f64 / GB as f64)
160 } else if bytes >= MB {
161 format!("{:.1} MB", bytes as f64 / MB as f64)
162 } else if bytes >= KB {
163 format!("{:.1} KB", bytes as f64 / KB as f64)
164 } else {
165 format!("{bytes} B")
166 }
167}
168
169pub fn format_uptime(seconds: u64) -> String {
170 let days = seconds / 86400;
171 let hours = (seconds % 86400) / 3600;
172 let minutes = (seconds % 3600) / 60;
173
174 if days > 0 {
175 format!("{days}d {hours}h {minutes}m")
176 } else if hours > 0 {
177 format!("{hours}h {minutes}m")
178 } else {
179 format!("{minutes}m")
180 }
181}
182
183#[derive(Debug)]
185pub enum ApiError {
186 Http(reqwest::Error),
187 Api { status: u16, message: String },
188 NotFound(String),
189 Auth(String),
190 Other(String),
191}
192
193impl fmt::Display for ApiError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 ApiError::Http(e) => write!(f, "HTTP error: {e}"),
197 ApiError::Api { status, message } => write!(f, "API error ({status}): {message}"),
198 ApiError::NotFound(msg) => write!(f, "Not found: {msg}"),
199 ApiError::Auth(msg) => write!(f, "Authentication error: {msg}"),
200 ApiError::Other(msg) => write!(f, "{msg}"),
201 }
202 }
203}
204
205impl std::error::Error for ApiError {}
206
207impl From<reqwest::Error> for ApiError {
208 fn from(e: reqwest::Error) -> Self {
209 if e.status()
210 .is_some_and(|s| s.as_u16() == 401 || s.as_u16() == 403)
211 {
212 ApiError::Auth(e.to_string())
213 } else if e.status().is_some_and(|s| s.as_u16() == 404) {
214 ApiError::NotFound(e.to_string())
215 } else {
216 ApiError::Http(e)
217 }
218 }
219}