switchbot_cli/
cli.rs

1use std::io::stdout;
2
3use switchbot_api::{CommandRequest, Device, DeviceList, SwitchBot};
4
5use crate::{Args, UserInput};
6
7#[derive(Debug, Default)]
8pub struct Cli {
9    args: Args,
10    switch_bot: SwitchBot,
11    current_device_indexes: Vec<usize>,
12}
13
14impl Cli {
15    pub fn new_from_args() -> Self {
16        Self {
17            args: Args::new_from_args(),
18            ..Default::default()
19        }
20    }
21
22    #[cfg(test)]
23    fn new_for_test(n_devices: usize) -> Self {
24        Self {
25            switch_bot: SwitchBot::new_for_test(n_devices),
26            ..Default::default()
27        }
28    }
29
30    fn devices(&self) -> &DeviceList {
31        self.switch_bot.devices()
32    }
33
34    fn has_current_device(&self) -> bool {
35        !self.current_device_indexes.is_empty()
36    }
37
38    fn current_devices(&self) -> Vec<&Device> {
39        self.current_device_indexes
40            .iter()
41            .map(|&index| &self.devices()[index])
42            .collect() // Collect the iterator into a Vec
43    }
44
45    pub async fn run(&mut self) -> anyhow::Result<()> {
46        if !self.args.alias_updates.is_empty() {
47            self.args.print_aliases();
48            self.args.save()?;
49            return Ok(());
50        }
51
52        self.switch_bot = self.args.create_switch_bot()?;
53        self.switch_bot.load_devices().await?;
54
55        if !self.args.commands.is_empty() {
56            self.execute_args(&self.args.commands.clone()).await?;
57            self.args.save()?;
58            return Ok(());
59        }
60
61        self.run_interactive().await?;
62
63        self.args.save()?;
64        Ok(())
65    }
66
67    async fn run_interactive(&mut self) -> anyhow::Result<()> {
68        let mut input = UserInput::new();
69        self.print_devices();
70        loop {
71            input.set_prompt(if self.has_current_device() {
72                "Command> "
73            } else {
74                "Device> "
75            });
76
77            let input_text = input.read_line()?;
78            match input_text {
79                "q" => break,
80                "" => {
81                    if self.has_current_device() {
82                        self.current_device_indexes.clear();
83                        self.print_devices();
84                        continue;
85                    }
86                    break;
87                }
88                _ => match self.execute(input_text).await {
89                    Ok(true) => self.print_devices(),
90                    Ok(false) => {}
91                    Err(error) => log::error!("{error}"),
92                },
93            }
94        }
95        Ok(())
96    }
97
98    fn print_devices(&self) {
99        if self.current_device_indexes.is_empty() {
100            for (i, device) in self.devices().iter().enumerate() {
101                println!("{}: {device}", i + 1);
102            }
103            return;
104        }
105
106        if self.current_device_indexes.len() >= 2 {
107            for (i, device) in self
108                .current_device_indexes
109                .iter()
110                .map(|i| (i, &self.devices()[*i]))
111            {
112                println!("{}: {device}", i + 1);
113            }
114            return;
115        }
116
117        let device = self.current_devices()[0];
118        print!("{device:#}");
119    }
120
121    async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
122        for command in list {
123            self.execute(command).await?;
124        }
125        Ok(())
126    }
127
128    async fn execute(&mut self, mut text: &str) -> anyhow::Result<bool> {
129        let alias_result: String;
130        if let Some(alias) = self.args.aliases.get(text) {
131            log::debug!(r#"alias: "{text}" -> "{alias}""#);
132            alias_result = alias.clone();
133            text = &alias_result;
134        }
135
136        let set_device_result = self.set_current_devices(text);
137        if set_device_result.is_ok() {
138            return Ok(true);
139        }
140        if self.has_current_device() {
141            if text == "status" {
142                self.update_status("").await?;
143                return Ok(false);
144            }
145            if let Some(key) = text.strip_prefix("status.") {
146                self.update_status(key).await?;
147                return Ok(false);
148            }
149            self.execute_command(&CommandRequest::from(text)).await?;
150            return Ok(false);
151        }
152        Err(set_device_result.unwrap_err())
153    }
154
155    fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
156        self.current_device_indexes = self.parse_device_indexes(text)?;
157        log::debug!("current_device_indexes={:?}", self.current_device_indexes);
158        Ok(())
159    }
160
161    fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
162        let values = value.split(',');
163        let mut indexes: Vec<usize> = Vec::new();
164        for s in values {
165            if let Some(alias) = self.args.aliases.get(s) {
166                indexes.extend(self.parse_device_indexes(alias)?);
167                continue;
168            }
169            indexes.push(self.parse_device_index(s)?);
170        }
171        indexes.sort();
172        indexes.dedup();
173        Ok(indexes)
174    }
175
176    fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
177        if let Ok(number) = value.parse::<usize>() {
178            if number > 0 && number <= self.devices().len() {
179                return Ok(number - 1);
180            }
181        }
182        self.devices()
183            .index_by_device_id(value)
184            .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
185    }
186
187    async fn execute_command(&self, command: &CommandRequest) -> anyhow::Result<()> {
188        let devices = self.current_devices();
189        for device in devices {
190            device.command(command).await?;
191        }
192        Ok(())
193    }
194
195    async fn update_status(&mut self, key: &str) -> anyhow::Result<()> {
196        for device in self.current_devices() {
197            device.update_status().await?;
198
199            if key.is_empty() {
200                device.write_status_to(stdout())?;
201            } else if let Some(value) = device.status_by_key(key) {
202                println!("{}", value);
203            } else {
204                log::error!(r#"No status key "{key}" for {device}"#);
205            }
206        }
207        Ok(())
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn parse_device_indexes() {
217        let cli = Cli::new_for_test(10);
218        assert!(cli.parse_device_indexes("").is_err());
219        assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
220        assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
221        assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
222        assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
223        // The result should be sorted.
224        assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![1, 3]);
225        assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![1, 3]);
226        // The result should be deduped.
227        assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
228    }
229
230    #[test]
231    fn parse_device_indexes_alias() {
232        let mut cli = Cli::new_for_test(10);
233        cli.args.aliases.insert("k".into(), "3,5".into());
234        assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
235        assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 3, 4]);
236        cli.args.aliases.insert("j".into(), "2,k".into());
237        assert_eq!(
238            cli.parse_device_indexes("1,j,4").unwrap(),
239            vec![0, 1, 2, 3, 4]
240        );
241        assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
242    }
243}