Skip to main content

mabi_cli/commands/
list.rs

1//! List command implementation.
2//!
3//! Lists devices, protocols, and other resources.
4
5use crate::context::CliContext;
6use crate::error::CliResult;
7use crate::output::{DeviceSummary, OutputFormat, StatusType, TableBuilder};
8use crate::runner::{Command, CommandOutput};
9use async_trait::async_trait;
10use mabi_core::prelude::*;
11use mabi_scenario::Scenario;
12use serde::Serialize;
13
14/// Resource type to list.
15#[derive(Debug, Clone, Copy, Default)]
16pub enum ListResource {
17    #[default]
18    Devices,
19    Protocols,
20    Points,
21    Scenarios,
22}
23
24impl ListResource {
25    pub fn from_str(s: &str) -> Option<Self> {
26        match s.to_lowercase().as_str() {
27            "devices" | "device" | "d" => Some(Self::Devices),
28            "protocols" | "protocol" | "p" => Some(Self::Protocols),
29            "points" | "point" => Some(Self::Points),
30            "scenarios" | "scenario" | "s" => Some(Self::Scenarios),
31            _ => None,
32        }
33    }
34}
35
36/// List command for displaying resources.
37pub struct ListCommand {
38    /// Resource type to list.
39    resource: ListResource,
40    /// Filter by protocol.
41    protocol: Option<Protocol>,
42    /// Filter by device ID pattern.
43    device_filter: Option<String>,
44    /// Maximum number of items to show.
45    limit: Option<usize>,
46}
47
48impl ListCommand {
49    /// Create a new list command.
50    pub fn new(resource: ListResource) -> Self {
51        Self {
52            resource,
53            protocol: None,
54            device_filter: None,
55            limit: None,
56        }
57    }
58
59    /// Filter by protocol.
60    pub fn with_protocol(mut self, protocol: Protocol) -> Self {
61        self.protocol = Some(protocol);
62        self
63    }
64
65    /// Filter by device ID pattern.
66    pub fn with_device_filter(mut self, pattern: impl Into<String>) -> Self {
67        self.device_filter = Some(pattern.into());
68        self
69    }
70
71    /// Set the maximum number of items.
72    pub fn with_limit(mut self, limit: usize) -> Self {
73        self.limit = Some(limit);
74        self
75    }
76
77    /// List devices.
78    async fn list_devices(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
79        let output = ctx.output();
80
81        // Get devices from engine if available
82        let devices: Vec<DeviceSummary> = {
83            let engine_guard = ctx.engine_ref().read().await;
84            if let Some(engine) = engine_guard.as_ref() {
85                engine
86                    .list_devices()
87                    .into_iter()
88                    .filter(|info| {
89                        // Protocol filter
90                        if let Some(ref proto) = self.protocol {
91                            if info.protocol != *proto {
92                                return false;
93                            }
94                        }
95                        // Device ID filter
96                        if let Some(ref pattern) = self.device_filter {
97                            if !info.id.contains(pattern) {
98                                return false;
99                            }
100                        }
101                        true
102                    })
103                    .take(self.limit.unwrap_or(usize::MAX))
104                    .map(|info| DeviceSummary {
105                        id: info.id.clone(),
106                        name: info.name.clone(),
107                        protocol: format!("{:?}", info.protocol),
108                        status: format!("{:?}", info.state),
109                        points: info.point_count,
110                        last_update: info.updated_at.to_rfc3339(),
111                    })
112                    .collect()
113            } else {
114                Vec::new()
115            }
116        };
117
118        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
119            output.write(&devices)?;
120            return Ok(CommandOutput::quiet_success());
121        }
122
123        output.header("Devices");
124
125        if devices.is_empty() {
126            output.info("No devices found");
127            if self.protocol.is_some() || self.device_filter.is_some() {
128                output.kv("Tip", "Try removing filters to see all devices");
129            }
130        } else {
131            let mut table = TableBuilder::new(output.colors_enabled())
132                .header(["ID", "Name", "Protocol", "Points", "Status"]);
133
134            for device in &devices {
135                let status_type = match device.status.as_str() {
136                    "Online" => StatusType::Success,
137                    "Offline" | "Error" => StatusType::Error,
138                    _ => StatusType::Neutral,
139                };
140                table = table.status_row(
141                    [
142                        &device.id,
143                        &device.name,
144                        &device.protocol,
145                        &device.points.to_string(),
146                        &device.status,
147                    ],
148                    status_type,
149                );
150            }
151            table.print();
152            output.kv("Total", devices.len());
153        }
154
155        Ok(CommandOutput::quiet_success())
156    }
157
158    /// List supported protocols.
159    async fn list_protocols(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
160        let output = ctx.output();
161
162        let protocols = vec![
163            ProtocolInfo {
164                name: "Modbus TCP".into(),
165                protocol: "modbus_tcp".into(),
166                default_port: 502,
167                description: "Modbus TCP/IP protocol".into(),
168                features: vec![
169                    "Read/Write Coils",
170                    "Read/Write Registers",
171                    "Multi-unit support",
172                ],
173            },
174            ProtocolInfo {
175                name: "Modbus RTU".into(),
176                protocol: "modbus_rtu".into(),
177                default_port: 0,
178                description: "Modbus RTU serial protocol".into(),
179                features: vec!["Serial communication", "CRC validation", "Multi-device bus"],
180            },
181            ProtocolInfo {
182                name: "OPC UA".into(),
183                protocol: "opcua".into(),
184                default_port: 4840,
185                description: "OPC Unified Architecture".into(),
186                features: vec!["Subscriptions", "History", "Security", "Address space"],
187            },
188            ProtocolInfo {
189                name: "BACnet/IP".into(),
190                protocol: "bacnet".into(),
191                default_port: 47808,
192                description: "BACnet over IP".into(),
193                features: vec![
194                    "Read/Write Properties",
195                    "COV Subscriptions",
196                    "BBMD",
197                    "Device discovery",
198                ],
199            },
200            ProtocolInfo {
201                name: "KNXnet/IP".into(),
202                protocol: "knx".into(),
203                default_port: 3671,
204                description: "KNX over IP".into(),
205                features: vec!["Tunneling", "Group addressing", "DPT support"],
206            },
207        ];
208
209        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
210            output.write(&protocols)?;
211            return Ok(CommandOutput::quiet_success());
212        }
213
214        output.header("Supported Protocols");
215
216        let mut table = TableBuilder::new(output.colors_enabled())
217            .header(["Protocol", "Name", "Port", "Features"]);
218
219        for proto in &protocols {
220            table = table.row([
221                &proto.protocol,
222                &proto.name,
223                &proto.default_port.to_string(),
224                &proto.features.join(", "),
225            ]);
226        }
227        table.print();
228
229        Ok(CommandOutput::quiet_success())
230    }
231
232    /// List data points.
233    async fn list_points(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
234        let output = ctx.output();
235
236        // Get points from devices
237        // Note: This is a simplified implementation that lists device info
238        // Full point listing would require device handles with point_definitions()
239        let points: Vec<PointInfo> = {
240            let engine_guard = ctx.engine_ref().read().await;
241            if let Some(engine) = engine_guard.as_ref() {
242                engine
243                    .list_devices()
244                    .into_iter()
245                    .filter(|info| {
246                        if let Some(ref pattern) = self.device_filter {
247                            info.id.contains(pattern)
248                        } else {
249                            true
250                        }
251                    })
252                    .flat_map(|info| {
253                        // For now, create placeholder points based on point_count
254                        // In real implementation, we would use device handles to get actual point definitions
255                        (0..info.point_count).map(move |i| PointInfo {
256                            device_id: info.id.clone(),
257                            point_id: format!("point_{}", i),
258                            data_type: "Unknown".into(),
259                            access: "ReadWrite".into(),
260                            description: String::new(),
261                        })
262                    })
263                    .take(self.limit.unwrap_or(usize::MAX))
264                    .collect()
265            } else {
266                Vec::new()
267            }
268        };
269
270        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
271            output.write(&points)?;
272            return Ok(CommandOutput::quiet_success());
273        }
274
275        output.header("Data Points");
276
277        if points.is_empty() {
278            output.info("No data points found");
279        } else {
280            let mut table = TableBuilder::new(output.colors_enabled()).header([
281                "Device",
282                "Point ID",
283                "Type",
284                "Access",
285                "Description",
286            ]);
287
288            for point in &points {
289                table = table.row([
290                    &point.device_id,
291                    &point.point_id,
292                    &point.data_type,
293                    &point.access,
294                    &point.description,
295                ]);
296            }
297            table.print();
298            output.kv("Total", points.len());
299        }
300
301        Ok(CommandOutput::quiet_success())
302    }
303
304    /// List scenarios.
305    async fn list_scenarios(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
306        let output = ctx.output();
307        let working_dir = ctx.working_dir().clone();
308
309        // Search for scenario files
310        let scenarios_dir = working_dir.join("scenarios");
311        let mut scenarios = Vec::new();
312
313        if scenarios_dir.exists() {
314            let mut entries = tokio::fs::read_dir(&scenarios_dir).await?;
315            while let Some(entry) = entries.next_entry().await? {
316                let path = entry.path();
317                if path
318                    .extension()
319                    .map_or(false, |e| e == "yaml" || e == "yml")
320                {
321                    if let Ok(content) = tokio::fs::read_to_string(&path).await {
322                        if let Ok(scenario) = serde_yaml::from_str::<Scenario>(&content) {
323                            scenarios.push(ScenarioInfo {
324                                name: scenario.name,
325                                file: path
326                                    .file_name()
327                                    .unwrap_or_default()
328                                    .to_string_lossy()
329                                    .into(),
330                                devices: scenario.devices.len(),
331                                events: scenario.events.len(),
332                                description: scenario.description,
333                            });
334                        }
335                    }
336                }
337
338                if let Some(limit) = self.limit {
339                    if scenarios.len() >= limit {
340                        break;
341                    }
342                }
343            }
344        }
345
346        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
347            output.write(&scenarios)?;
348            return Ok(CommandOutput::quiet_success());
349        }
350
351        output.header("Scenarios");
352
353        if scenarios.is_empty() {
354            output.info("No scenarios found");
355            output.kv("Search path", scenarios_dir.display());
356        } else {
357            let mut table = TableBuilder::new(output.colors_enabled())
358                .header(["Name", "File", "Devices", "Events"]);
359
360            for scenario in &scenarios {
361                table = table.row([
362                    &scenario.name,
363                    &scenario.file,
364                    &scenario.devices.to_string(),
365                    &scenario.events.to_string(),
366                ]);
367            }
368            table.print();
369            output.kv("Total", scenarios.len());
370        }
371
372        Ok(CommandOutput::quiet_success())
373    }
374}
375
376#[async_trait]
377impl Command for ListCommand {
378    fn name(&self) -> &str {
379        "list"
380    }
381
382    fn description(&self) -> &str {
383        "List devices, protocols, points, or scenarios"
384    }
385
386    async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
387        match self.resource {
388            ListResource::Devices => self.list_devices(ctx).await,
389            ListResource::Protocols => self.list_protocols(ctx).await,
390            ListResource::Points => self.list_points(ctx).await,
391            ListResource::Scenarios => self.list_scenarios(ctx).await,
392        }
393    }
394}
395
396#[derive(Debug, Clone, Serialize)]
397struct ProtocolInfo {
398    name: String,
399    protocol: String,
400    default_port: u16,
401    description: String,
402    features: Vec<&'static str>,
403}
404
405#[derive(Debug, Clone, Serialize)]
406struct PointInfo {
407    device_id: String,
408    point_id: String,
409    data_type: String,
410    access: String,
411    description: String,
412}
413
414#[derive(Debug, Clone, Serialize)]
415struct ScenarioInfo {
416    name: String,
417    file: String,
418    devices: usize,
419    events: usize,
420    description: String,
421}