switchbot_cli/
cli.rs

1use std::{io::stdout, iter::zip};
2
3use itertools::Itertools;
4use switchbot_api::{CommandRequest, Device, DeviceList, SwitchBot};
5
6use crate::{Args, UserInput};
7
8#[derive(Debug, Default)]
9pub struct Cli {
10    args: Args,
11    switch_bot: SwitchBot,
12    current_device_indexes: Vec<usize>,
13}
14
15impl Cli {
16    pub fn new_from_args() -> Self {
17        Self {
18            args: Args::new_from_args(),
19            ..Default::default()
20        }
21    }
22
23    #[cfg(test)]
24    fn new_for_test(n_devices: usize) -> Self {
25        Self {
26            switch_bot: SwitchBot::new_for_test(n_devices),
27            ..Default::default()
28        }
29    }
30
31    fn devices(&self) -> &DeviceList {
32        self.switch_bot.devices()
33    }
34
35    fn has_current_device(&self) -> bool {
36        !self.current_device_indexes.is_empty()
37    }
38
39    fn current_devices_as<'a, T, F>(&'a self, f: F) -> impl Iterator<Item = T> + 'a
40    where
41        F: Fn(usize) -> T + 'a,
42    {
43        self.current_device_indexes
44            .iter()
45            .map(move |&index| f(index))
46    }
47
48    fn current_devices(&self) -> impl Iterator<Item = &Device> {
49        self.current_devices_as(|index| &self.devices()[index])
50    }
51
52    fn current_devices_with_index(&self) -> impl Iterator<Item = (usize, &Device)> {
53        self.current_devices_as(|index| (index, &self.devices()[index]))
54    }
55
56    fn first_current_device(&self) -> &Device {
57        &self.devices()[self.current_device_indexes[0]]
58    }
59
60    pub async fn run(&mut self) -> anyhow::Result<()> {
61        self.run_core().await?;
62        self.args.save()?;
63        Ok(())
64    }
65
66    async fn run_core(&mut self) -> anyhow::Result<()> {
67        if !self.args.alias_updates.is_empty() {
68            self.args.print_aliases();
69            if self.args.commands.is_empty() {
70                return Ok(());
71            }
72        }
73
74        self.switch_bot = self.args.create_switch_bot()?;
75        self.switch_bot.load_devices().await?;
76
77        if !self.args.commands.is_empty() {
78            self.execute_args(&self.args.commands.clone()).await?;
79            return Ok(());
80        }
81
82        self.run_interactive().await?;
83        Ok(())
84    }
85
86    async fn run_interactive(&mut self) -> anyhow::Result<()> {
87        let mut input = UserInput::new();
88        self.print_devices();
89        loop {
90            input.set_prompt(if self.has_current_device() {
91                "Command> "
92            } else {
93                "Device> "
94            });
95
96            let input_text = input.read_line()?;
97            match input_text {
98                "q" => break,
99                "" => {
100                    if self.has_current_device() {
101                        self.current_device_indexes.clear();
102                        self.print_devices();
103                        continue;
104                    }
105                    break;
106                }
107                _ => match self.execute(input_text).await {
108                    Ok(true) => self.print_devices(),
109                    Ok(false) => {}
110                    Err(error) => log::error!("{error}"),
111                },
112            }
113        }
114        Ok(())
115    }
116
117    fn print_devices(&self) {
118        if !self.has_current_device() {
119            for (i, device) in self.devices().iter().enumerate() {
120                println!("{}: {device}", i + 1);
121            }
122            return;
123        }
124
125        if self.current_device_indexes.len() >= 2 {
126            for (i, device) in self.current_devices_with_index() {
127                println!("{}: {device}", i + 1);
128            }
129            return;
130        }
131
132        let device = self.first_current_device();
133        print!("{device:#}");
134    }
135
136    async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
137        for command in list {
138            self.execute(command).await?;
139        }
140        Ok(())
141    }
142
143    async fn execute(&mut self, text: &str) -> anyhow::Result<bool> {
144        if let Some(alias) = self.args.aliases.get(text) {
145            log::debug!(r#"alias: "{text}" -> "{alias}""#);
146            return self.execute_no_alias(&alias.clone()).await;
147        }
148        self.execute_no_alias(text).await
149    }
150
151    async fn execute_no_alias(&mut self, text: &str) -> anyhow::Result<bool> {
152        let set_device_result = self.set_current_devices(text);
153        if set_device_result.is_ok() {
154            return Ok(true);
155        }
156        if self.has_current_device() {
157            if text == "status" {
158                self.update_status("").await?;
159                return Ok(false);
160            }
161            if let Some(key) = text.strip_prefix("status.") {
162                self.update_status(key).await?;
163                return Ok(false);
164            }
165            if self.execute_if_expr(text).await? {
166                return Ok(false);
167            }
168            self.execute_command(text).await?;
169            return Ok(false);
170        }
171        Err(set_device_result.unwrap_err())
172    }
173
174    fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
175        self.current_device_indexes = self.parse_device_indexes(text)?;
176        log::debug!("current_device_indexes={:?}", self.current_device_indexes);
177        Ok(())
178    }
179
180    fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
181        let values = value.split(',');
182        let mut indexes: Vec<usize> = Vec::new();
183        for s in values {
184            if let Some(alias) = self.args.aliases.get(s) {
185                indexes.extend(self.parse_device_indexes(alias)?);
186                continue;
187            }
188            indexes.push(self.parse_device_index(s)?);
189        }
190        indexes = indexes.into_iter().unique().collect::<Vec<_>>();
191        Ok(indexes)
192    }
193
194    fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
195        if let Ok(number) = value.parse::<usize>() {
196            if number > 0 && number <= self.devices().len() {
197                return Ok(number - 1);
198            }
199        }
200        self.devices()
201            .index_by_device_id(value)
202            .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
203    }
204
205    async fn execute_if_expr(&mut self, expr: &str) -> anyhow::Result<bool> {
206        if let Some((condition, then_command, else_command)) = Self::parse_if_expr(expr) {
207            let device = self.first_current_device();
208            device.update_status().await?;
209            let eval_result = device.eval_condition(condition)?;
210            let command = if eval_result {
211                then_command
212            } else {
213                else_command
214            };
215            log::debug!("if: {condition} is {eval_result}, execute {command}");
216            if let Some(alias) = self.args.aliases.get(command) {
217                self.execute_command(alias.as_str()).await?;
218            } else {
219                self.execute_command(command).await?;
220            }
221            return Ok(true);
222        }
223        Ok(false)
224    }
225
226    fn parse_if_expr(text: &str) -> Option<(&str, &str, &str)> {
227        if let Some(text) = text.strip_prefix("if") {
228            if let Some(sep) = text.chars().nth(0) {
229                if sep.is_alphanumeric() {
230                    return None;
231                }
232                let fields: Vec<&str> = text[1..].split_terminator(sep).collect();
233                match fields.len() {
234                    2 => return Some((fields[0], fields[1], "")),
235                    3 => return Some((fields[0], fields[1], fields[2])),
236                    _ => {}
237                }
238            }
239        }
240        None
241    }
242
243    async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
244        if text.is_empty() {
245            return Ok(());
246        }
247        let command = CommandRequest::from(text);
248        let (_, results) = async_scoped::TokioScope::scope_and_block(|s| {
249            for device in self.current_devices() {
250                s.spawn(device.command(&command));
251            }
252        });
253
254        let results = Self::merge_join_errors(results);
255        for (result, is_last_error) in results {
256            if is_last_error {
257                return result;
258            }
259            if let Err(error) = result {
260                log::error!("{error}");
261                continue;
262            }
263        }
264        Ok(())
265    }
266
267    async fn update_status(&self, key: &str) -> anyhow::Result<()> {
268        let (_, results) = async_scoped::TokioScope::scope_and_block(|s| {
269            for device in self.current_devices() {
270                s.spawn(device.update_status());
271            }
272        });
273
274        let results = Self::merge_join_errors(results);
275        for (device, (result, is_last_error)) in zip(self.current_devices(), results) {
276            if is_last_error {
277                return result;
278            }
279            if let Err(error) = result {
280                log::error!("{error}");
281                continue;
282            }
283            if key.is_empty() {
284                device.write_status_to(stdout())?;
285            } else if let Some(value) = device.status_by_key(key) {
286                println!("{}", value);
287            } else {
288                log::error!(r#"No status key "{key}" for {device}"#);
289            }
290        }
291        Ok(())
292    }
293
294    fn merge_join_errors(
295        results: Vec<Result<anyhow::Result<()>, tokio::task::JoinError>>,
296    ) -> Vec<(anyhow::Result<()>, bool)> {
297        let mut output: Vec<(anyhow::Result<()>, bool)> = vec![];
298        let mut last_error_index = None;
299        for result in results {
300            let result = result.unwrap_or_else(|error| Err(error.into()));
301            if result.is_err() {
302                last_error_index = Some(output.len());
303            }
304            output.push((result, false));
305        }
306        if let Some(last_error_index) = last_error_index {
307            output[last_error_index].1 = true;
308        }
309        output
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn parse_device_indexes() {
319        let cli = Cli::new_for_test(10);
320        assert!(cli.parse_device_indexes("").is_err());
321        assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
322        assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
323        assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
324        assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
325        // The result should not be sorted.
326        assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
327        assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
328        // The result should be unique.
329        assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
330        assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
331    }
332
333    #[test]
334    fn parse_device_indexes_alias() {
335        let mut cli = Cli::new_for_test(10);
336        cli.args.aliases.insert("k".into(), "3,5".into());
337        assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
338        assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
339        cli.args.aliases.insert("j".into(), "2,k".into());
340        assert_eq!(
341            cli.parse_device_indexes("1,j,4").unwrap(),
342            vec![0, 1, 2, 4, 3]
343        );
344        assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
345    }
346
347    #[test]
348    fn parse_if_expr() {
349        assert_eq!(Cli::parse_if_expr(""), None);
350        assert_eq!(Cli::parse_if_expr("a"), None);
351        assert_eq!(Cli::parse_if_expr("if"), None);
352        assert_eq!(Cli::parse_if_expr("if/a"), None);
353        assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
354        assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
355        assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
356        // The separator can be any characters as long as they're consistent.
357        assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
358        assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
359        // But non-alphanumeric.
360        assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
361    }
362}