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 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 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 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 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 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
441 }
442}