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