1use std::{collections::HashMap, 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.args.process()?;
76 self.run_core().await?;
77 self.args.save()?;
78 Ok(())
79 }
80
81 async fn run_core(&mut self) -> anyhow::Result<()> {
82 let mut is_interactive = true;
83 if !self.args.alias_updates.is_empty() {
84 self.args.aliases.print();
85 is_interactive = false;
86 }
87
88 if !self.args.commands.is_empty() {
89 self.ensure_devices().await?;
90 self.execute_args(&self.args.commands.clone()).await?;
91 } else if is_interactive {
92 self.ensure_devices().await?;
93 self.run_interactive().await?;
94 }
95 Ok(())
96 }
97
98 async fn run_interactive(&mut self) -> anyhow::Result<()> {
99 let mut input = UserInput::new();
100 self.print_devices();
101 loop {
102 input.set_prompt(if self.has_current_device() {
103 "Command> "
104 } else {
105 "Device> "
106 });
107
108 let input_text = input.read_line()?;
109 match input_text {
110 "q" => break,
111 "" => {
112 if self.has_current_device() {
113 self.current_device_indexes.clear();
114 self.print_devices();
115 continue;
116 }
117 break;
118 }
119 _ => match self.execute(input_text).await {
120 Ok(true) => self.print_devices(),
121 Ok(false) => {}
122 Err(error) => log::error!("{error}"),
123 },
124 }
125 }
126 Ok(())
127 }
128
129 fn print_devices(&self) {
130 if !self.has_current_device() {
131 self.print_all_devices();
132 return;
133 }
134
135 if self.current_device_indexes.len() >= 2 {
136 self.print_devices_with_index(self.current_devices_with_index());
137 return;
138 }
139
140 let device = self.first_current_device();
141 print!("{device:#}");
142 }
143
144 fn print_all_devices(&self) {
145 self.print_devices_with_index(self.devices().iter().enumerate());
146 }
147
148 fn print_devices_with_index<'a>(&self, iter: impl IntoIterator<Item = (usize, &'a Device)>) {
149 let reverse_aliases = self.args.aliases.reverse_map();
150 for (i, device) in iter {
151 self.print_device(device, i, &reverse_aliases);
152 }
153 }
154
155 fn print_device(
156 &self,
157 device: &Device,
158 index: usize,
159 reverse_aliases: &HashMap<&str, Vec<&str>>,
160 ) {
161 let index = index + 1;
162 let mut aliases: Vec<&str> = Vec::new();
163 if let Some(list) = reverse_aliases.get(index.to_string().as_str()) {
164 aliases.extend(list);
165 }
166 if let Some(list) = reverse_aliases.get(device.device_id()) {
167 aliases.extend(list);
168 }
169 if !aliases.is_empty() {
170 aliases.sort();
171 println!("{index}: {}={device}", aliases.iter().join("="));
172 } else {
173 println!("{index}: {device}");
174 }
175 }
176
177 const COMMAND_URL: &str =
178 "https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands";
179 const COMMAND_IR_URL: &str = "https://github.com/OpenWonderLabs/SwitchBotAPI#command-set-for-virtual-infrared-remote-devices";
180
181 async fn print_help(&mut self) -> anyhow::Result<()> {
182 if self.help.is_none() {
183 self.help = Some(Help::load().await?);
184 }
185 let device = self.first_current_device();
186 let command_helps = self.help.as_ref().unwrap().command_helps(device);
187 let help_url = if device.is_remote() {
188 Self::COMMAND_IR_URL
189 } else {
190 Self::COMMAND_URL
191 };
192 if command_helps.is_empty() {
193 anyhow::bail!(
194 r#"No help for "{}". Please see {} for more information"#,
195 device.device_type_or_remote_type(),
196 help_url
197 )
198 }
199 for command_help in command_helps {
200 println!("{command_help}");
201 }
202 println!("Please see {help_url} for more information");
203 Ok(())
204 }
205
206 async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
207 for command in list {
208 self.execute(command).await?;
209 }
210 Ok(())
211 }
212
213 async fn execute(&mut self, text: &str) -> anyhow::Result<bool> {
215 let text = &self.args.aliases.expand(text);
216 let set_device_result = self.set_current_devices(text);
217 if set_device_result.is_ok() {
218 return Ok(true);
219 }
220 if self.execute_global_builtin_command(text)? {
221 return Ok(false);
222 }
223 if self.has_current_device() {
224 if self.execute_if_expr(text).await? {
225 return Ok(false);
226 }
227 if text == "help" {
228 self.print_help().await?;
229 return Ok(false);
230 }
231 self.execute_command(text).await?;
232 return Ok(false);
233 }
234 Err(set_device_result.unwrap_err())
235 }
236
237 fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
238 self.current_device_indexes = self.parse_device_indexes(text)?;
239 log::debug!("current_device_indexes={:?}", self.current_device_indexes);
240 Ok(())
241 }
242
243 fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
244 let values = value.split(',');
245 let mut indexes: Vec<usize> = Vec::new();
246 for s in values {
247 if let Some(alias) = self.args.aliases.get(s) {
248 indexes.extend(self.parse_device_indexes(alias)?);
249 continue;
250 }
251 indexes.push(self.parse_device_index(s)?);
252 }
253 indexes = indexes.into_iter().unique().collect::<Vec<_>>();
254 Ok(indexes)
255 }
256
257 fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
258 if let Ok(number) = value.parse::<usize>()
259 && number > 0
260 && number <= self.devices().len()
261 {
262 return Ok(number - 1);
263 }
264 self.devices()
265 .index_by_device_id(value)
266 .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
267 }
268
269 async fn execute_if_expr(&mut self, expr: &str) -> anyhow::Result<bool> {
270 assert!(self.has_current_device());
271 if let Some((condition, then_command, else_command)) = Self::parse_if_expr(expr) {
272 let (device, expr) = self.device_expr(condition);
273 device.update_status().await?;
274 let eval_result = device.eval_condition(expr)?;
275 let command = if eval_result {
276 then_command
277 } else {
278 else_command
279 };
280 log::debug!("if: {condition} is {eval_result}, execute {command}");
281 Box::pin(self.execute(command)).await?;
282 return Ok(true);
283 }
284 Ok(false)
285 }
286
287 fn parse_if_expr(text: &str) -> Option<(&str, &str, &str)> {
288 if let Some(text) = text.strip_prefix("if")
289 && let Some(sep) = text.chars().nth(0)
290 {
291 if sep.is_alphanumeric() {
292 return None;
293 }
294 let fields: Vec<&str> = text[1..].split_terminator(sep).collect();
295 match fields.len() {
296 2 => return Some((fields[0], fields[1], "")),
297 3 => return Some((fields[0], fields[1], fields[2])),
298 _ => {}
299 }
300 }
301 None
302 }
303
304 fn device_expr<'a>(&'a self, expr: &'a str) -> (&'a Device, &'a str) {
305 if let Some((device, expr)) = expr.split_once('.')
306 && let Ok(device_indexes) = self.parse_device_indexes(device)
307 {
308 return (&self.devices()[device_indexes[0]], expr);
309 }
310 (self.first_current_device(), expr)
311 }
312
313 fn execute_global_builtin_command(&mut self, text: &str) -> anyhow::Result<bool> {
314 if text == "devices" {
315 self.print_all_devices();
316 return Ok(true);
317 }
318 if text == "alias" {
319 self.args.aliases.print();
320 return Ok(true);
321 }
322 if let Some(rest) = text.strip_prefix("alias ") {
323 let rest = rest.trim();
324 if rest.is_empty() {
325 self.args.aliases.print();
326 } else {
327 self.args.aliases.update(rest);
328 }
329 return Ok(true);
330 }
331 Ok(false)
332 }
333
334 async fn execute_device_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
335 assert!(self.has_current_device());
336 if text == "status" {
337 self.update_status("").await?;
338 return Ok(true);
339 }
340 if let Some(key) = text.strip_prefix("status.") {
341 self.update_status(key).await?;
342 return Ok(true);
343 }
344 Ok(false)
345 }
346
347 async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
348 assert!(self.has_current_device());
349 if text.is_empty() {
350 return Ok(());
351 }
352 if self.execute_device_builtin_command(text).await? {
353 return Ok(());
354 }
355 let command = CommandRequest::from(text);
356 self.for_each_selected_device(|device| device.command(&command), |_| Ok(()))
357 .await?;
358 Ok(())
359 }
360
361 async fn update_status(&self, key: &str) -> anyhow::Result<()> {
362 self.for_each_selected_device(
363 |device: &Device| device.update_status(),
364 |device| {
365 if key.is_empty() {
366 device.write_status_to(stdout())?;
367 } else if let Some(value) = device.status_by_key(key) {
368 println!("{value}");
369 } else {
370 log::error!(r#"No status key "{key}" for {device}"#);
371 }
372 Ok(())
373 },
374 )
375 .await?;
376 Ok(())
377 }
378
379 async fn for_each_selected_device<'a, 'b, FnAsync, Fut>(
380 &'a self,
381 fn_async: FnAsync,
382 fn_post: impl Fn(&Device) -> anyhow::Result<()>,
383 ) -> anyhow::Result<()>
384 where
385 FnAsync: Fn(&'a Device) -> Fut + Send + Sync,
386 Fut: Future<Output = anyhow::Result<()>> + Send + 'b,
387 {
388 assert!(self.has_current_device());
389
390 let results = if self.num_current_devices() < self.args.parallel_threshold {
391 log::debug!("for_each: sequential ({})", self.num_current_devices());
392 let mut results = Vec::with_capacity(self.num_current_devices());
393 for device in self.current_devices() {
394 results.push(fn_async(device).await);
395 }
396 results
397 } else {
398 log::debug!("for_each: parallel ({})", self.num_current_devices());
399 let (_, join_results) = async_scoped::TokioScope::scope_and_block(|s| {
400 for device in self.current_devices() {
401 s.spawn(fn_async(device));
402 }
403 });
404 join_results
405 .into_iter()
406 .map(|result| result.unwrap_or_else(|error| Err(error.into())))
407 .collect()
408 };
409
410 let last_error_index = results.iter().rposition(|result| result.is_err());
411 for (i, (device, result)) in zip(self.current_devices(), results).enumerate() {
412 match result {
413 Ok(_) => fn_post(device)?,
414 Err(error) => {
415 if i == last_error_index.unwrap() {
416 return Err(error);
417 }
418 log::error!("{error}");
419 }
420 }
421 }
422 Ok(())
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn parse_device_indexes() {
432 let cli = Cli::new_for_test(10);
433 assert!(cli.parse_device_indexes("").is_err());
434 assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
435 assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
436 assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
437 assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
438 assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
440 assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
441 assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
443 assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
444 }
445
446 #[test]
447 fn parse_device_indexes_alias() {
448 let mut cli = Cli::new_for_test(10);
449 cli.args.aliases.insert("k".into(), "3,5".into());
450 assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
451 assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
452 cli.args.aliases.insert("j".into(), "2,k".into());
453 assert_eq!(
454 cli.parse_device_indexes("1,j,4").unwrap(),
455 vec![0, 1, 2, 4, 3]
456 );
457 assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
458 }
459
460 #[test]
461 fn parse_if_expr() {
462 assert_eq!(Cli::parse_if_expr(""), None);
463 assert_eq!(Cli::parse_if_expr("a"), None);
464 assert_eq!(Cli::parse_if_expr("if"), None);
465 assert_eq!(Cli::parse_if_expr("if/a"), None);
466 assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
467 assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
468 assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
469 assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
471 assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
472 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
474 }
475
476 #[test]
477 fn command_alias() {
478 let mut cli = Cli::new_for_test(10);
479 assert_eq!(cli.args.aliases.len(), 0);
480
481 assert!(cli.execute_global_builtin_command("alias a=b").unwrap());
483 assert_eq!(cli.args.aliases.len(), 1);
484 assert_eq!(cli.args.aliases.get("a").unwrap(), "b");
485
486 assert!(cli.execute_global_builtin_command("alias a=c").unwrap());
488 assert_eq!(cli.args.aliases.len(), 1);
489 assert_eq!(cli.args.aliases.get("a").unwrap(), "c");
490
491 assert!(cli.execute_global_builtin_command("alias a=").unwrap());
493 assert_eq!(cli.args.aliases.len(), 0);
494
495 assert!(cli.execute_global_builtin_command("alias").unwrap());
497 assert_eq!(cli.args.aliases.len(), 0);
498
499 assert!(cli.execute_global_builtin_command("alias a=").unwrap());
501 assert_eq!(cli.args.aliases.len(), 0);
502
503 cli.args.aliases.insert("a".into(), "b".into());
505 assert!(cli.execute_global_builtin_command("alias a").unwrap());
506 assert_eq!(cli.args.aliases.len(), 0);
507 }
508}