switchbot_cli/
cli.rs

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