switchbot_cli/
cli.rs

1use std::{future::Future, io::stdout, iter::zip};
2
3use itertools::Itertools;
4use switchbot_api::{CommandRequest, Device, DeviceList, Help, 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    help: Option<Help>,
14}
15
16impl Cli {
17    pub fn new_from_args() -> Self {
18        Self {
19            args: Args::new_from_args(),
20            ..Default::default()
21        }
22    }
23
24    #[cfg(test)]
25    fn new_for_test(n_devices: usize) -> Self {
26        Self {
27            switch_bot: SwitchBot::new_for_test(n_devices),
28            ..Default::default()
29        }
30    }
31
32    fn devices(&self) -> &DeviceList {
33        self.switch_bot.devices()
34    }
35
36    fn has_current_device(&self) -> bool {
37        !self.current_device_indexes.is_empty()
38    }
39
40    fn num_current_devices(&self) -> usize {
41        self.current_device_indexes.len()
42    }
43
44    fn current_devices_as<'a, T, F>(&'a self, f: F) -> impl Iterator<Item = T> + 'a
45    where
46        F: Fn(usize) -> T + 'a,
47    {
48        self.current_device_indexes
49            .iter()
50            .map(move |&index| f(index))
51    }
52
53    fn current_devices(&self) -> impl Iterator<Item = &Device> {
54        self.current_devices_as(|index| &self.devices()[index])
55    }
56
57    fn current_devices_with_index(&self) -> impl Iterator<Item = (usize, &Device)> {
58        self.current_devices_as(|index| (index, &self.devices()[index]))
59    }
60
61    fn first_current_device(&self) -> &Device {
62        &self.devices()[self.current_device_indexes[0]]
63    }
64
65    async fn ensure_devices(&mut self) -> anyhow::Result<()> {
66        if self.devices().is_empty() {
67            self.switch_bot = self.args.create_switch_bot()?;
68            self.switch_bot.load_devices().await?;
69            log::debug!("ensure_devices: {} devices", self.devices().len());
70        }
71        Ok(())
72    }
73
74    pub async fn run(&mut self) -> anyhow::Result<()> {
75        self.run_core().await?;
76        self.args.save()?;
77        Ok(())
78    }
79
80    async fn run_core(&mut self) -> anyhow::Result<()> {
81        let mut is_interactive = true;
82        if !self.args.alias_updates.is_empty() {
83            self.args.print_aliases();
84            is_interactive = false;
85        }
86
87        if !self.args.commands.is_empty() {
88            self.ensure_devices().await?;
89            self.execute_args(&self.args.commands.clone()).await?;
90        } else if is_interactive {
91            self.ensure_devices().await?;
92            self.run_interactive().await?;
93        }
94        Ok(())
95    }
96
97    async fn run_interactive(&mut self) -> anyhow::Result<()> {
98        let mut input = UserInput::new();
99        self.print_devices();
100        loop {
101            input.set_prompt(if self.has_current_device() {
102                "Command> "
103            } else {
104                "Device> "
105            });
106
107            let input_text = input.read_line()?;
108            match input_text {
109                "q" => break,
110                "" => {
111                    if self.has_current_device() {
112                        self.current_device_indexes.clear();
113                        self.print_devices();
114                        continue;
115                    }
116                    break;
117                }
118                _ => match self.execute(input_text).await {
119                    Ok(true) => self.print_devices(),
120                    Ok(false) => {}
121                    Err(error) => log::error!("{error}"),
122                },
123            }
124        }
125        Ok(())
126    }
127
128    fn print_devices(&self) {
129        if !self.has_current_device() {
130            self.print_all_devices();
131            return;
132        }
133
134        if self.current_device_indexes.len() >= 2 {
135            for (i, device) in self.current_devices_with_index() {
136                println!("{}: {device}", i + 1);
137            }
138            return;
139        }
140
141        let device = self.first_current_device();
142        print!("{device:#}");
143    }
144
145    fn print_all_devices(&self) {
146        for (i, device) in self.devices().iter().enumerate() {
147            println!("{}: {device}", i + 1);
148        }
149    }
150
151    const COMMAND_URL: &str =
152        "https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands";
153    const COMMAND_IR_URL: &str = "https://github.com/OpenWonderLabs/SwitchBotAPI#command-set-for-virtual-infrared-remote-devices";
154
155    async fn print_help(&mut self) -> anyhow::Result<()> {
156        if self.help.is_none() {
157            self.help = Some(Help::load().await?);
158        }
159        let device = self.first_current_device();
160        let command_helps = self.help.as_ref().unwrap().command_helps(device);
161        let help_url = if device.is_remote() {
162            Self::COMMAND_IR_URL
163        } else {
164            Self::COMMAND_URL
165        };
166        if command_helps.is_empty() {
167            anyhow::bail!(
168                r#"No help for "{}". Please see {} for more information"#,
169                device.device_type_or_remote_type(),
170                help_url
171            )
172        }
173        for command_help in command_helps {
174            println!("{}", command_help);
175        }
176        println!("Please see {} for more information", help_url);
177        Ok(())
178    }
179
180    async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
181        for command in list {
182            self.execute(command).await?;
183        }
184        Ok(())
185    }
186
187    async fn execute(&mut self, text: &str) -> anyhow::Result<bool> {
188        if let Some(alias) = self.args.aliases.get(text) {
189            log::debug!(r#"alias: "{text}" -> "{alias}""#);
190            return self.execute_no_alias(&alias.clone()).await;
191        }
192        self.execute_no_alias(text).await
193    }
194
195    /// Returns `true` if the current devices are changed.
196    async fn execute_no_alias(&mut self, text: &str) -> anyhow::Result<bool> {
197        let set_device_result = self.set_current_devices(text);
198        if set_device_result.is_ok() {
199            return Ok(true);
200        }
201        if self.execute_global_builtin_command(text)? {
202            return Ok(false);
203        }
204        if self.has_current_device() {
205            if self.execute_if_expr(text).await? {
206                return Ok(false);
207            }
208            if text == "help" {
209                self.print_help().await?;
210                return Ok(false);
211            }
212            self.execute_command(text).await?;
213            return Ok(false);
214        }
215        Err(set_device_result.unwrap_err())
216    }
217
218    fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
219        self.current_device_indexes = self.parse_device_indexes(text)?;
220        log::debug!("current_device_indexes={:?}", self.current_device_indexes);
221        Ok(())
222    }
223
224    fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
225        let values = value.split(',');
226        let mut indexes: Vec<usize> = Vec::new();
227        for s in values {
228            if let Some(alias) = self.args.aliases.get(s) {
229                indexes.extend(self.parse_device_indexes(alias)?);
230                continue;
231            }
232            indexes.push(self.parse_device_index(s)?);
233        }
234        indexes = indexes.into_iter().unique().collect::<Vec<_>>();
235        Ok(indexes)
236    }
237
238    fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
239        if let Ok(number) = value.parse::<usize>() {
240            if number > 0 && number <= self.devices().len() {
241                return Ok(number - 1);
242            }
243        }
244        self.devices()
245            .index_by_device_id(value)
246            .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
247    }
248
249    async fn execute_if_expr(&mut self, expr: &str) -> anyhow::Result<bool> {
250        assert!(self.has_current_device());
251        if let Some((condition, then_command, else_command)) = Self::parse_if_expr(expr) {
252            let (device, expr) = self.device_expr(condition);
253            device.update_status().await?;
254            let eval_result = device.eval_condition(expr)?;
255            let command = if eval_result {
256                then_command
257            } else {
258                else_command
259            };
260            log::debug!("if: {condition} is {eval_result}, execute {command}");
261            Box::pin(self.execute(command)).await?;
262            return Ok(true);
263        }
264        Ok(false)
265    }
266
267    fn parse_if_expr(text: &str) -> Option<(&str, &str, &str)> {
268        if let Some(text) = text.strip_prefix("if") {
269            if let Some(sep) = text.chars().nth(0) {
270                if sep.is_alphanumeric() {
271                    return None;
272                }
273                let fields: Vec<&str> = text[1..].split_terminator(sep).collect();
274                match fields.len() {
275                    2 => return Some((fields[0], fields[1], "")),
276                    3 => return Some((fields[0], fields[1], fields[2])),
277                    _ => {}
278                }
279            }
280        }
281        None
282    }
283
284    fn device_expr<'a>(&'a self, expr: &'a str) -> (&'a Device, &'a str) {
285        if let Some((device, expr)) = expr.split_once('.') {
286            if let Ok(device_indexes) = self.parse_device_indexes(device) {
287                return (&self.devices()[device_indexes[0]], expr);
288            }
289        }
290        (self.first_current_device(), expr)
291    }
292
293    fn execute_global_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
294        if text == "devices" {
295            self.print_all_devices();
296            return Ok(true);
297        }
298        Ok(false)
299    }
300
301    async fn execute_device_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
302        assert!(self.has_current_device());
303        if text == "status" {
304            self.update_status("").await?;
305            return Ok(true);
306        }
307        if let Some(key) = text.strip_prefix("status.") {
308            self.update_status(key).await?;
309            return Ok(true);
310        }
311        Ok(false)
312    }
313
314    async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
315        assert!(self.has_current_device());
316        if text.is_empty() {
317            return Ok(());
318        }
319        if self.execute_device_builtin_command(text).await? {
320            return Ok(());
321        }
322        let command = CommandRequest::from(text);
323        self.for_each_selected_device(|device| device.command(&command), |_| Ok(()))
324            .await?;
325        Ok(())
326    }
327
328    async fn update_status(&self, key: &str) -> anyhow::Result<()> {
329        self.for_each_selected_device(
330            |device: &Device| device.update_status(),
331            |device| {
332                if key.is_empty() {
333                    device.write_status_to(stdout())?;
334                } else if let Some(value) = device.status_by_key(key) {
335                    println!("{}", value);
336                } else {
337                    log::error!(r#"No status key "{key}" for {device}"#);
338                }
339                Ok(())
340            },
341        )
342        .await?;
343        Ok(())
344    }
345
346    async fn for_each_selected_device<'a, 'b, FnAsync, Fut>(
347        &'a self,
348        fn_async: FnAsync,
349        fn_post: impl Fn(&Device) -> anyhow::Result<()>,
350    ) -> anyhow::Result<()>
351    where
352        FnAsync: Fn(&'a Device) -> Fut + Send + Sync,
353        Fut: Future<Output = anyhow::Result<()>> + Send + 'b,
354    {
355        assert!(self.has_current_device());
356
357        let results = if self.num_current_devices() < self.args.parallel_threshold {
358            log::debug!("for_each: sequential ({})", self.num_current_devices());
359            let mut results = Vec::with_capacity(self.num_current_devices());
360            for device in self.current_devices() {
361                results.push(fn_async(device).await);
362            }
363            results
364        } else {
365            log::debug!("for_each: parallel ({})", self.num_current_devices());
366            let (_, join_results) = async_scoped::TokioScope::scope_and_block(|s| {
367                for device in self.current_devices() {
368                    s.spawn(fn_async(device));
369                }
370            });
371            join_results
372                .into_iter()
373                .map(|result| result.unwrap_or_else(|error| Err(error.into())))
374                .collect()
375        };
376
377        let last_error_index = results.iter().rposition(|result| result.is_err());
378        for (i, (device, result)) in zip(self.current_devices(), results).enumerate() {
379            match result {
380                Ok(_) => fn_post(device)?,
381                Err(error) => {
382                    if i == last_error_index.unwrap() {
383                        return Err(error);
384                    }
385                    log::error!("{error}");
386                }
387            }
388        }
389        Ok(())
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn parse_device_indexes() {
399        let cli = Cli::new_for_test(10);
400        assert!(cli.parse_device_indexes("").is_err());
401        assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
402        assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
403        assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
404        assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
405        // The result should not be sorted.
406        assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
407        assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
408        // The result should be unique.
409        assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
410        assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
411    }
412
413    #[test]
414    fn parse_device_indexes_alias() {
415        let mut cli = Cli::new_for_test(10);
416        cli.args.aliases.insert("k".into(), "3,5".into());
417        assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
418        assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
419        cli.args.aliases.insert("j".into(), "2,k".into());
420        assert_eq!(
421            cli.parse_device_indexes("1,j,4").unwrap(),
422            vec![0, 1, 2, 4, 3]
423        );
424        assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
425    }
426
427    #[test]
428    fn parse_if_expr() {
429        assert_eq!(Cli::parse_if_expr(""), None);
430        assert_eq!(Cli::parse_if_expr("a"), None);
431        assert_eq!(Cli::parse_if_expr("if"), None);
432        assert_eq!(Cli::parse_if_expr("if/a"), None);
433        assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
434        assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
435        assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
436        // The separator can be any characters as long as they're consistent.
437        assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
438        assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
439        // But non-alphanumeric.
440        assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
441    }
442}