Skip to main content

unifi_cli/commands/
devices.rs

1use owo_colors::OwoColorize;
2
3use crate::api::{Device, UnifiClient, format_mac, format_uptime};
4use crate::commands::ports::{self, PortRow};
5use crate::output::{OutputConfig, use_color};
6
7pub struct Pagination {
8    pub limit: usize,
9    pub offset: usize,
10    /// Field names already validated against `fields::DEVICES_LIST`.
11    pub fields: Option<Vec<String>>,
12}
13
14fn render_devices(devices: &[Device], out: &OutputConfig) {
15    if out.is_json() {
16        out.print_data(
17            &serde_json::to_string_pretty(
18                &devices
19                    .iter()
20                    .map(|d| {
21                        serde_json::json!({
22                            "name": d.name,
23                            "model": d.model,
24                            "mac": d.mac_address,
25                            "ip": d.ip_address,
26                            "state": d.state,
27                            "firmware": d.firmware_version,
28                        })
29                    })
30                    .collect::<Vec<_>>(),
31            )
32            .expect("failed to serialize JSON"),
33        );
34    } else {
35        let color = use_color();
36
37        // Compute dynamic column widths from data
38        let col = |min: usize, label_len: usize, vals: Vec<usize>| -> usize {
39            vals.into_iter().max().unwrap_or(0).max(label_len).max(min) + 2
40        };
41        let names: Vec<&str> = devices
42            .iter()
43            .map(|d| d.name.as_deref().unwrap_or("-"))
44            .collect();
45        let models: Vec<&str> = devices
46            .iter()
47            .map(|d| d.model.as_deref().unwrap_or("-"))
48            .collect();
49
50        let name_w = col(4, 4, names.iter().map(|n| n.len()).collect());
51        let model_w = col(5, 5, models.iter().map(|m| m.len()).collect());
52        let total_w = name_w + model_w + 19 + 15 + 10 + 10;
53
54        let header = format!(
55            "{:<name_w$} {:<model_w$} {:<19} {:<15} {:<10} {}",
56            "Name", "Model", "MAC", "IP", "State", "Firmware"
57        );
58        if color {
59            println!("{}", header.bold());
60            println!("{}", "-".repeat(total_w).dimmed());
61        } else {
62            println!("{header}");
63            println!("{}", "-".repeat(total_w));
64        }
65
66        for d in devices {
67            let name = d.name.as_deref().unwrap_or("-");
68            let model = d.model.as_deref().unwrap_or("-");
69            let mac = d
70                .mac_address
71                .as_deref()
72                .map(format_mac)
73                .unwrap_or_else(|| "-".into());
74            let ip = d.ip_address.as_deref().unwrap_or("-");
75            let state = d.state.as_deref().unwrap_or("-");
76            let fw = d.firmware_version.as_deref().unwrap_or("-");
77            let name_pad = name_w - 1;
78            let model_pad = model_w;
79
80            if color {
81                println!(
82                    " {:<name_pad$} {:<model_pad$} {:<19} {:<15} {:<10} {}",
83                    name.bold(),
84                    model,
85                    mac.dimmed(),
86                    ip,
87                    state,
88                    fw,
89                );
90            } else {
91                println!(
92                    " {:<name_pad$} {:<model_pad$} {:<19} {:<15} {:<10} {}",
93                    name, model, mac, ip, state, fw
94                );
95            }
96        }
97    }
98    out.print_message(&format!("\n{} devices", devices.len()));
99}
100
101pub async fn list(
102    client: &mut UnifiClient,
103    out: OutputConfig,
104    watch: Option<u64>,
105    pagination: Pagination,
106) -> Result<(), Box<dyn std::error::Error>> {
107    if let Some(interval) = watch {
108        use crossterm::execute;
109        use crossterm::terminal::EnterAlternateScreen;
110
111        let mut stdout = std::io::stdout();
112        execute!(stdout, EnterAlternateScreen)?;
113
114        loop {
115            execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
116            execute!(
117                stdout,
118                crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
119            )?;
120            eprintln!("Every {interval}s | devices list (press Ctrl+C to exit)\n");
121            match client.list_devices().await {
122                Ok(devices) => {
123                    render_devices(&devices, &out);
124                }
125                Err(e) => {
126                    eprintln!("Error: {e}");
127                }
128            }
129            tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
130        }
131    } else {
132        let devices = client.list_devices().await?;
133        let total = devices.len();
134        let paginated: Vec<Device> = devices
135            .into_iter()
136            .skip(pagination.offset)
137            .take(pagination.limit)
138            .collect();
139        if out.is_json() {
140            let items: Vec<serde_json::Value> = paginated
141                .iter()
142                .map(|d| {
143                    let mut obj = serde_json::json!({
144                        "name": d.name,
145                        "model": d.model,
146                        "mac": d.mac_address,
147                        "ip": d.ip_address,
148                        "state": d.state,
149                        "firmware": d.firmware_version,
150                    });
151                    if let Some(ref keep) = pagination.fields {
152                        let map = obj.as_object_mut().expect("device is a JSON object");
153                        map.retain(|k, _| keep.iter().any(|f| f == k));
154                    }
155                    obj
156                })
157                .collect();
158            out.print_data(
159                &serde_json::to_string_pretty(&serde_json::json!({
160                    "items": items,
161                    "total": total,
162                    "limit": pagination.limit,
163                    "offset": pagination.offset,
164                }))
165                .expect("failed to serialize JSON"),
166            );
167        } else {
168            render_devices(&paginated, &out);
169        }
170        Ok(())
171    }
172}
173
174pub async fn show(
175    client: &UnifiClient,
176    mac: &str,
177    out: OutputConfig,
178) -> Result<(), Box<dyn std::error::Error>> {
179    let d = client.get_device_detail(mac).await?;
180
181    if out.is_json() {
182        out.print_data(&serde_json::to_string_pretty(&serde_json::json!({
183            "name": d.name,
184            "model": d.model,
185            "mac": d.mac,
186            "ip": d.ip,
187            "state": d.state_str(),
188            "firmware": d.version,
189            "version": d.version,
190            "uptime": d.uptime,
191            "num_sta": d.num_sta,
192        }))?);
193        return Ok(());
194    }
195
196    let color = use_color();
197    let label = |l: &str| -> String {
198        if color {
199            format!("{}", l.dimmed())
200        } else {
201            l.to_string()
202        }
203    };
204
205    let name = d.name.as_deref().unwrap_or("Device");
206    if color {
207        println!("{}", name.bold());
208    } else {
209        println!("{name}");
210    }
211
212    println!(
213        "  {}  {}",
214        label("Model:   "),
215        d.model.as_deref().unwrap_or("-")
216    );
217    println!(
218        "  {}  {}",
219        label("MAC:     "),
220        d.mac
221            .as_deref()
222            .map(format_mac)
223            .unwrap_or_else(|| "-".into())
224    );
225    println!(
226        "  {}  {}",
227        label("IP:      "),
228        d.ip.as_deref().unwrap_or("-")
229    );
230    println!("  {}  {}", label("State:   "), d.state_str());
231
232    if let Some(ref v) = d.version {
233        println!("  {}  {v}", label("Firmware:"));
234    }
235    if let Some(uptime) = d.uptime {
236        println!("  {}  {}", label("Uptime:  "), format_uptime(uptime));
237    }
238    if let Some(num_sta) = d.num_sta {
239        println!("  {}  {num_sta}", label("Clients: "));
240    }
241
242    Ok(())
243}
244
245pub async fn restart(
246    client: &UnifiClient,
247    mac: &str,
248    out: OutputConfig,
249) -> Result<(), Box<dyn std::error::Error>> {
250    client.restart_device(mac).await?;
251    out.print_result(
252        &serde_json::json!({"status": "ok", "action": "restart", "mac": format_mac(mac)}),
253        &format!("Restarting {}", format_mac(mac)),
254    );
255    Ok(())
256}
257
258/// Alias for `ports list <MAC>`. Deliberately keeps the historical bare JSON
259/// array shape: `ports list` emits the paginated `{items,total,...}` envelope,
260/// but changing this one from array to object would break every consumer
261/// indexing the top level.
262pub async fn ports(
263    client: &UnifiClient,
264    mac: &str,
265    out: OutputConfig,
266) -> Result<(), Box<dyn std::error::Error>> {
267    let device = client.get_device_ports(mac).await?;
268
269    if device.port_table.is_empty() {
270        out.print_message("No port table available for this device (not a switch or router)");
271        if out.is_json() {
272            out.print_data("[]");
273        }
274        return Ok(());
275    }
276
277    let devices = vec![device];
278    // Historical label for a device with neither `name` nor `model`; `ports
279    // list` / `ports find` keep "-" via `collect_rows`.
280    let rows = ports::collect_rows_with_fallback(&devices, "Device");
281
282    if out.is_json() {
283        let items: Vec<serde_json::Value> = rows.iter().map(ports::row_json).collect();
284        out.print_data(&serde_json::to_string_pretty(&items)?);
285    } else {
286        let label = &rows[0].device_name;
287        out.print_message(&format!("Ports for {label}:\n"));
288        let refs: Vec<&PortRow> = rows.iter().collect();
289        let dev_w = ports::device_col_width(&refs);
290        ports::render_text(&refs, false, dev_w, &out);
291    }
292    Ok(())
293}
294
295pub async fn upgrade(
296    client: &UnifiClient,
297    mac: &str,
298    out: OutputConfig,
299) -> Result<(), Box<dyn std::error::Error>> {
300    client.upgrade_device(mac).await?;
301    out.print_result(
302        &serde_json::json!({"status": "ok", "action": "upgrade", "mac": format_mac(mac)}),
303        &format!("Upgrading firmware on {}", format_mac(mac)),
304    );
305    Ok(())
306}
307
308pub async fn locate(
309    client: &UnifiClient,
310    mac: &str,
311    off: bool,
312    out: OutputConfig,
313) -> Result<(), Box<dyn std::error::Error>> {
314    client.locate_device(mac, !off).await?;
315    let action = if off { "locate_off" } else { "locate_on" };
316    let msg = if off {
317        format!("Stopped locating {}", format_mac(mac))
318    } else {
319        format!("Locating {} (LED blinking)", format_mac(mac))
320    };
321    out.print_result(
322        &serde_json::json!({"status": "ok", "action": action, "mac": format_mac(mac)}),
323        &msg,
324    );
325    Ok(())
326}