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.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.print_aliases();
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 for (i, device) in self.current_devices_with_index() {
137 println!("{}: {device}", i + 1);
138 }
139 return;
140 }
141
142 let device = self.first_current_device();
143 print!("{device:#}");
144 }
145
146 fn print_all_devices(&self) {
147 for (i, device) in self.devices().iter().enumerate() {
148 println!("{}: {device}", i + 1);
149 }
150 }
151
152 const COMMAND_URL: &str =
153 "https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands";
154 const COMMAND_IR_URL: &str = "https://github.com/OpenWonderLabs/SwitchBotAPI#command-set-for-virtual-infrared-remote-devices";
155
156 async fn print_help(&mut self) -> anyhow::Result<()> {
157 if self.help.is_none() {
158 self.help = Some(Help::load().await?);
159 }
160 let device = self.first_current_device();
161 let command_helps = self.help.as_ref().unwrap().command_helps(device);
162 let help_url = if device.is_remote() {
163 Self::COMMAND_IR_URL
164 } else {
165 Self::COMMAND_URL
166 };
167 if command_helps.is_empty() {
168 anyhow::bail!(
169 r#"No help for "{}". Please see {} for more information"#,
170 device.device_type_or_remote_type(),
171 help_url
172 )
173 }
174 for command_help in command_helps {
175 println!("{command_help}");
176 }
177 println!("Please see {help_url} for more information");
178 Ok(())
179 }
180
181 async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
182 for command in list {
183 self.execute(command).await?;
184 }
185 Ok(())
186 }
187
188 async fn execute(&mut self, text: &str) -> anyhow::Result<bool> {
189 if let Some(alias) = self.args.aliases.get(text) {
190 log::debug!(r#"alias: "{text}" -> "{alias}""#);
191 return self.execute_no_alias(&alias.clone()).await;
192 }
193 self.execute_no_alias(text).await
194 }
195
196 async fn execute_no_alias(&mut self, text: &str) -> anyhow::Result<bool> {
198 let set_device_result = self.set_current_devices(text);
199 if set_device_result.is_ok() {
200 return Ok(true);
201 }
202 if self.execute_global_builtin_command(text)? {
203 return Ok(false);
204 }
205 if self.has_current_device() {
206 if self.execute_if_expr(text).await? {
207 return Ok(false);
208 }
209 if text == "help" {
210 self.print_help().await?;
211 return Ok(false);
212 }
213 self.execute_command(text).await?;
214 return Ok(false);
215 }
216 Err(set_device_result.unwrap_err())
217 }
218
219 fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
220 self.current_device_indexes = self.parse_device_indexes(text)?;
221 log::debug!("current_device_indexes={:?}", self.current_device_indexes);
222 Ok(())
223 }
224
225 fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
226 let values = value.split(',');
227 let mut indexes: Vec<usize> = Vec::new();
228 for s in values {
229 if let Some(alias) = self.args.aliases.get(s) {
230 indexes.extend(self.parse_device_indexes(alias)?);
231 continue;
232 }
233 indexes.push(self.parse_device_index(s)?);
234 }
235 indexes = indexes.into_iter().unique().collect::<Vec<_>>();
236 Ok(indexes)
237 }
238
239 fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
240 if let Ok(number) = value.parse::<usize>()
241 && number > 0
242 && number <= self.devices().len()
243 {
244 return Ok(number - 1);
245 }
246 self.devices()
247 .index_by_device_id(value)
248 .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
249 }
250
251 async fn execute_if_expr(&mut self, expr: &str) -> anyhow::Result<bool> {
252 assert!(self.has_current_device());
253 if let Some((condition, then_command, else_command)) = Self::parse_if_expr(expr) {
254 let (device, expr) = self.device_expr(condition);
255 device.update_status().await?;
256 let eval_result = device.eval_condition(expr)?;
257 let command = if eval_result {
258 then_command
259 } else {
260 else_command
261 };
262 log::debug!("if: {condition} is {eval_result}, execute {command}");
263 Box::pin(self.execute(command)).await?;
264 return Ok(true);
265 }
266 Ok(false)
267 }
268
269 fn parse_if_expr(text: &str) -> Option<(&str, &str, &str)> {
270 if let Some(text) = text.strip_prefix("if")
271 && let Some(sep) = text.chars().nth(0)
272 {
273 if sep.is_alphanumeric() {
274 return None;
275 }
276 let fields: Vec<&str> = text[1..].split_terminator(sep).collect();
277 match fields.len() {
278 2 => return Some((fields[0], fields[1], "")),
279 3 => return Some((fields[0], fields[1], fields[2])),
280 _ => {}
281 }
282 }
283 None
284 }
285
286 fn device_expr<'a>(&'a self, expr: &'a str) -> (&'a Device, &'a str) {
287 if let Some((device, expr)) = expr.split_once('.')
288 && let Ok(device_indexes) = self.parse_device_indexes(device)
289 {
290 return (&self.devices()[device_indexes[0]], expr);
291 }
292 (self.first_current_device(), expr)
293 }
294
295 fn execute_global_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
296 if text == "devices" {
297 self.print_all_devices();
298 return Ok(true);
299 }
300 Ok(false)
301 }
302
303 async fn execute_device_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
304 assert!(self.has_current_device());
305 if text == "status" {
306 self.update_status("").await?;
307 return Ok(true);
308 }
309 if let Some(key) = text.strip_prefix("status.") {
310 self.update_status(key).await?;
311 return Ok(true);
312 }
313 Ok(false)
314 }
315
316 async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
317 assert!(self.has_current_device());
318 if text.is_empty() {
319 return Ok(());
320 }
321 if self.execute_device_builtin_command(text).await? {
322 return Ok(());
323 }
324 let command = CommandRequest::from(text);
325 self.for_each_selected_device(|device| device.command(&command), |_| Ok(()))
326 .await?;
327 Ok(())
328 }
329
330 async fn update_status(&self, key: &str) -> anyhow::Result<()> {
331 self.for_each_selected_device(
332 |device: &Device| device.update_status(),
333 |device| {
334 if key.is_empty() {
335 device.write_status_to(stdout())?;
336 } else if let Some(value) = device.status_by_key(key) {
337 println!("{value}");
338 } else {
339 log::error!(r#"No status key "{key}" for {device}"#);
340 }
341 Ok(())
342 },
343 )
344 .await?;
345 Ok(())
346 }
347
348 async fn for_each_selected_device<'a, 'b, FnAsync, Fut>(
349 &'a self,
350 fn_async: FnAsync,
351 fn_post: impl Fn(&Device) -> anyhow::Result<()>,
352 ) -> anyhow::Result<()>
353 where
354 FnAsync: Fn(&'a Device) -> Fut + Send + Sync,
355 Fut: Future<Output = anyhow::Result<()>> + Send + 'b,
356 {
357 assert!(self.has_current_device());
358
359 let results = if self.num_current_devices() < self.args.parallel_threshold {
360 log::debug!("for_each: sequential ({})", self.num_current_devices());
361 let mut results = Vec::with_capacity(self.num_current_devices());
362 for device in self.current_devices() {
363 results.push(fn_async(device).await);
364 }
365 results
366 } else {
367 log::debug!("for_each: parallel ({})", self.num_current_devices());
368 let (_, join_results) = async_scoped::TokioScope::scope_and_block(|s| {
369 for device in self.current_devices() {
370 s.spawn(fn_async(device));
371 }
372 });
373 join_results
374 .into_iter()
375 .map(|result| result.unwrap_or_else(|error| Err(error.into())))
376 .collect()
377 };
378
379 let last_error_index = results.iter().rposition(|result| result.is_err());
380 for (i, (device, result)) in zip(self.current_devices(), results).enumerate() {
381 match result {
382 Ok(_) => fn_post(device)?,
383 Err(error) => {
384 if i == last_error_index.unwrap() {
385 return Err(error);
386 }
387 log::error!("{error}");
388 }
389 }
390 }
391 Ok(())
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn parse_device_indexes() {
401 let cli = Cli::new_for_test(10);
402 assert!(cli.parse_device_indexes("").is_err());
403 assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
404 assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
405 assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
406 assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
407 assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
409 assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
410 assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
412 assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
413 }
414
415 #[test]
416 fn parse_device_indexes_alias() {
417 let mut cli = Cli::new_for_test(10);
418 cli.args.aliases.insert("k".into(), "3,5".into());
419 assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
420 assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
421 cli.args.aliases.insert("j".into(), "2,k".into());
422 assert_eq!(
423 cli.parse_device_indexes("1,j,4").unwrap(),
424 vec![0, 1, 2, 4, 3]
425 );
426 assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
427 }
428
429 #[test]
430 fn parse_if_expr() {
431 assert_eq!(Cli::parse_if_expr(""), None);
432 assert_eq!(Cli::parse_if_expr("a"), None);
433 assert_eq!(Cli::parse_if_expr("if"), None);
434 assert_eq!(Cli::parse_if_expr("if/a"), None);
435 assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
436 assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
437 assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
438 assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
440 assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
441 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
443 }
444}