1use owo_colors::OwoColorize;
2
3use crate::api::{Device, UnifiClient, format_bytes, format_mac, format_uptime};
4use crate::output::{OutputConfig, use_color};
5
6pub struct Pagination {
7 pub limit: usize,
8 pub offset: usize,
9 pub fields: Option<String>,
10}
11
12fn render_devices(devices: &[Device], out: &OutputConfig) {
13 if out.is_json() {
14 out.print_data(
15 &serde_json::to_string_pretty(
16 &devices
17 .iter()
18 .map(|d| {
19 serde_json::json!({
20 "name": d.name,
21 "model": d.model,
22 "mac": d.mac_address,
23 "ip": d.ip_address,
24 "state": d.state,
25 "firmware": d.firmware_version,
26 })
27 })
28 .collect::<Vec<_>>(),
29 )
30 .expect("failed to serialize JSON"),
31 );
32 } else {
33 let color = use_color();
34
35 let col = |min: usize, label_len: usize, vals: Vec<usize>| -> usize {
37 vals.into_iter().max().unwrap_or(0).max(label_len).max(min) + 2
38 };
39 let names: Vec<&str> = devices
40 .iter()
41 .map(|d| d.name.as_deref().unwrap_or("-"))
42 .collect();
43 let models: Vec<&str> = devices
44 .iter()
45 .map(|d| d.model.as_deref().unwrap_or("-"))
46 .collect();
47
48 let name_w = col(4, 4, names.iter().map(|n| n.len()).collect());
49 let model_w = col(5, 5, models.iter().map(|m| m.len()).collect());
50 let total_w = name_w + model_w + 19 + 15 + 10 + 10;
51
52 let header = format!(
53 "{:<name_w$} {:<model_w$} {:<19} {:<15} {:<10} {}",
54 "Name", "Model", "MAC", "IP", "State", "Firmware"
55 );
56 if color {
57 println!("{}", header.bold());
58 println!("{}", "-".repeat(total_w).dimmed());
59 } else {
60 println!("{header}");
61 println!("{}", "-".repeat(total_w));
62 }
63
64 for d in devices {
65 let name = d.name.as_deref().unwrap_or("-");
66 let model = d.model.as_deref().unwrap_or("-");
67 let mac = d
68 .mac_address
69 .as_deref()
70 .map(format_mac)
71 .unwrap_or_else(|| "-".into());
72 let ip = d.ip_address.as_deref().unwrap_or("-");
73 let state = d.state.as_deref().unwrap_or("-");
74 let fw = d.firmware_version.as_deref().unwrap_or("-");
75 let name_pad = name_w - 1;
76 let model_pad = model_w;
77
78 if color {
79 println!(
80 " {:<name_pad$} {:<model_pad$} {:<19} {:<15} {:<10} {}",
81 name.bold(),
82 model,
83 mac.dimmed(),
84 ip,
85 state,
86 fw,
87 );
88 } else {
89 println!(
90 " {:<name_pad$} {:<model_pad$} {:<19} {:<15} {:<10} {}",
91 name, model, mac, ip, state, fw
92 );
93 }
94 }
95 }
96 out.print_message(&format!("\n{} devices", devices.len()));
97}
98
99pub async fn list(
100 client: &mut UnifiClient,
101 out: OutputConfig,
102 watch: Option<u64>,
103 pagination: Pagination,
104) -> Result<(), Box<dyn std::error::Error>> {
105 if let Some(interval) = watch {
106 use crossterm::execute;
107 use crossterm::terminal::EnterAlternateScreen;
108
109 let mut stdout = std::io::stdout();
110 execute!(stdout, EnterAlternateScreen)?;
111
112 loop {
113 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
114 execute!(
115 stdout,
116 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
117 )?;
118 eprintln!("Every {interval}s | devices list (press Ctrl+C to exit)\n");
119 match client.list_devices().await {
120 Ok(devices) => {
121 render_devices(&devices, &out);
122 }
123 Err(e) => {
124 eprintln!("Error: {e}");
125 }
126 }
127 tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
128 }
129 } else {
130 let devices = client.list_devices().await?;
131 let total = devices.len();
132 let paginated: Vec<Device> = devices
133 .into_iter()
134 .skip(pagination.offset)
135 .take(pagination.limit)
136 .collect();
137 if out.is_json() {
138 let items: Vec<serde_json::Value> = paginated
139 .iter()
140 .map(|d| {
141 let mut obj = serde_json::json!({
142 "name": d.name,
143 "model": d.model,
144 "mac": d.mac_address,
145 "ip": d.ip_address,
146 "state": d.state,
147 "firmware": d.firmware_version,
148 });
149 if let Some(ref fields_str) = pagination.fields {
150 let keep: Vec<&str> = fields_str.split(',').map(str::trim).collect();
151 let map = obj.as_object_mut().unwrap();
152 map.retain(|k, _| keep.contains(&k.as_str()));
153 }
154 obj
155 })
156 .collect();
157 out.print_data(
158 &serde_json::to_string_pretty(&serde_json::json!({
159 "items": items,
160 "total": total,
161 "limit": pagination.limit,
162 "offset": pagination.offset,
163 }))
164 .expect("failed to serialize JSON"),
165 );
166 } else {
167 render_devices(&paginated, &out);
168 }
169 Ok(())
170 }
171}
172
173pub async fn show(
174 client: &UnifiClient,
175 mac: &str,
176 out: OutputConfig,
177) -> Result<(), Box<dyn std::error::Error>> {
178 let d = client.get_device_detail(mac).await?;
179
180 if out.is_json() {
181 out.print_data(&serde_json::to_string_pretty(&serde_json::json!({
182 "name": d.name,
183 "model": d.model,
184 "mac": d.mac,
185 "ip": d.ip,
186 "state": d.state_str(),
187 "version": d.version,
188 "uptime": d.uptime,
189 "num_sta": d.num_sta,
190 }))?);
191 return Ok(());
192 }
193
194 let color = use_color();
195 let label = |l: &str| -> String {
196 if color {
197 format!("{}", l.dimmed())
198 } else {
199 l.to_string()
200 }
201 };
202
203 let name = d.name.as_deref().unwrap_or("Device");
204 if color {
205 println!("{}", name.bold());
206 } else {
207 println!("{name}");
208 }
209
210 println!(
211 " {} {}",
212 label("Model: "),
213 d.model.as_deref().unwrap_or("-")
214 );
215 println!(
216 " {} {}",
217 label("MAC: "),
218 d.mac
219 .as_deref()
220 .map(format_mac)
221 .unwrap_or_else(|| "-".into())
222 );
223 println!(
224 " {} {}",
225 label("IP: "),
226 d.ip.as_deref().unwrap_or("-")
227 );
228 println!(" {} {}", label("State: "), d.state_str());
229
230 if let Some(ref v) = d.version {
231 println!(" {} {v}", label("Firmware:"));
232 }
233 if let Some(uptime) = d.uptime {
234 println!(" {} {}", label("Uptime: "), format_uptime(uptime));
235 }
236 if let Some(num_sta) = d.num_sta {
237 println!(" {} {num_sta}", label("Clients: "));
238 }
239
240 Ok(())
241}
242
243pub async fn restart(
244 client: &UnifiClient,
245 mac: &str,
246 out: OutputConfig,
247) -> Result<(), Box<dyn std::error::Error>> {
248 client.restart_device(mac).await?;
249 out.print_result(
250 &serde_json::json!({"status": "ok", "action": "restart", "mac": format_mac(mac)}),
251 &format!("Restarting {}", format_mac(mac)),
252 );
253 Ok(())
254}
255
256pub async fn ports(
257 client: &UnifiClient,
258 mac: &str,
259 out: OutputConfig,
260) -> Result<(), Box<dyn std::error::Error>> {
261 let device = client.get_device_ports(mac).await?;
262
263 if device.port_table.is_empty() {
264 out.print_message("No port table available for this device (not a switch or router)");
265 if out.is_json() {
266 out.print_data("[]");
267 }
268 return Ok(());
269 }
270
271 if out.is_json() {
272 out.print_data(
273 &serde_json::to_string_pretty(
274 &device
275 .port_table
276 .iter()
277 .map(|p| {
278 serde_json::json!({
279 "port_idx": p.port_idx,
280 "name": p.name,
281 "media": p.media,
282 "up": p.up,
283 "speed": p.speed,
284 "full_duplex": p.full_duplex,
285 "poe_enable": p.poe_enable,
286 "poe_power": p.poe_power,
287 "port_poe": p.port_poe,
288 "tx_bytes": p.tx_bytes,
289 "rx_bytes": p.rx_bytes,
290 })
291 })
292 .collect::<Vec<_>>(),
293 )
294 .expect("failed to serialize JSON"),
295 );
296 } else {
297 let device_label = device
298 .name
299 .as_deref()
300 .unwrap_or(device.model.as_deref().unwrap_or("Device"));
301 out.print_message(&format!("Ports for {device_label}:\n"));
302
303 let color = use_color();
304 let header = format!(
305 "{:<6} {:<16} {:<6} {:<10} {:<8} {:>10} {:>10}",
306 "Port", "Name", "Link", "Speed", "PoE", "TX", "RX"
307 );
308 if color {
309 println!("{}", header.bold());
310 println!("{}", "-".repeat(70).dimmed());
311 } else {
312 println!("{header}");
313 println!("{}", "-".repeat(70));
314 }
315
316 for p in &device.port_table {
317 let port = p
318 .port_idx
319 .map(|i| i.to_string())
320 .unwrap_or_else(|| "-".into());
321 let name = p.name.as_deref().unwrap_or("-");
322 let link = if p.up { "up" } else { "down" };
323 let speed = if p.up {
324 match p.speed {
325 Some(s) => {
326 let duplex = if p.full_duplex { "FD" } else { "HD" };
327 format!("{s}{duplex}")
328 }
329 None => "up".into(),
330 }
331 } else {
332 "down".into()
333 };
334 let poe = if p.poe_enable {
335 match p.poe_power {
336 Some(w) if w > 0.0 => format!("{w:.1}W"),
337 _ => "on".into(),
338 }
339 } else if p.port_poe {
340 "off".into()
341 } else {
342 "-".into()
343 };
344 let tx = p.tx_bytes.map(format_bytes).unwrap_or_else(|| "-".into());
345 let rx = p.rx_bytes.map(format_bytes).unwrap_or_else(|| "-".into());
346
347 if color {
348 let link_display = if p.up {
349 format!("{}", "up".green())
350 } else {
351 format!("{}", "down".dimmed())
352 };
353 println!(
354 " {:<5} {:<16} {:<6} {:<10} {:<8} {:>10} {:>10}",
355 port, name, link_display, speed, poe, tx, rx
356 );
357 } else {
358 println!(
359 " {:<5} {:<16} {:<6} {:<10} {:<8} {:>10} {:>10}",
360 port, name, link, speed, poe, tx, rx
361 );
362 }
363 }
364 }
365 out.print_message(&format!("\n{} ports", device.port_table.len()));
366 Ok(())
367}
368
369pub async fn upgrade(
370 client: &UnifiClient,
371 mac: &str,
372 out: OutputConfig,
373) -> Result<(), Box<dyn std::error::Error>> {
374 client.upgrade_device(mac).await?;
375 out.print_result(
376 &serde_json::json!({"status": "ok", "action": "upgrade", "mac": format_mac(mac)}),
377 &format!("Upgrading firmware on {}", format_mac(mac)),
378 );
379 Ok(())
380}
381
382pub async fn locate(
383 client: &UnifiClient,
384 mac: &str,
385 off: bool,
386 out: OutputConfig,
387) -> Result<(), Box<dyn std::error::Error>> {
388 client.locate_device(mac, !off).await?;
389 let action = if off { "locate_off" } else { "locate_on" };
390 let msg = if off {
391 format!("Stopped locating {}", format_mac(mac))
392 } else {
393 format!("Locating {} (LED blinking)", format_mac(mac))
394 };
395 out.print_result(
396 &serde_json::json!({"status": "ok", "action": action, "mac": format_mac(mac)}),
397 &msg,
398 );
399 Ok(())
400}