switchbot_api/
device.rs

1use std::{
2    collections::HashMap,
3    fmt::Display,
4    io,
5    sync::{Arc, RwLock, RwLockReadGuard, Weak},
6    thread,
7    time::{Duration, Instant},
8};
9
10use super::*;
11
12/// A device in the SwitchBot API.
13///
14/// For the details of fields, please refer to the [devices] section
15/// of the API documentation.
16///
17/// [devices]: https://github.com/OpenWonderLabs/SwitchBotAPI#devices
18#[derive(Debug, Default, serde::Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct Device {
21    device_id: String,
22    #[serde(default)] // Missing in the status response.
23    device_name: String,
24    #[serde(default)]
25    device_type: String,
26    #[serde(default)]
27    remote_type: String,
28    hub_device_id: String,
29
30    #[serde(flatten)]
31    extra: HashMap<String, serde_json::Value>,
32
33    #[serde(skip)]
34    status: RwLock<HashMap<String, serde_json::Value>>,
35
36    #[serde(skip)]
37    service: Weak<SwitchBotService>,
38
39    #[serde(skip)]
40    last_command_time: RwLock<Option<Instant>>,
41}
42
43static MIN_INTERVAL_FOR_REMOTE_DEVICES: RwLock<Duration> = RwLock::new(Duration::from_millis(500));
44
45impl Device {
46    pub fn set_default_min_internal_for_remote_devices(min_interval: Duration) {
47        *MIN_INTERVAL_FOR_REMOTE_DEVICES.write().unwrap() = min_interval;
48    }
49
50    pub(crate) fn new_for_test(index: usize) -> Self {
51        Self {
52            device_id: format!("device{index}"),
53            device_name: format!("Device {index}"),
54            device_type: "test".into(),
55            ..Default::default()
56        }
57    }
58
59    /// The device ID.
60    pub fn device_id(&self) -> &str {
61        &self.device_id
62    }
63
64    /// The device name.
65    /// This is the name configured in the SwitchBot app.
66    pub fn device_name(&self) -> &str {
67        &self.device_name
68    }
69
70    /// True if this device is an infrared remote device.
71    pub fn is_remote(&self) -> bool {
72        !self.remote_type.is_empty()
73    }
74
75    /// The device type.
76    /// This is empty if this is an infrared remote device.
77    pub fn device_type(&self) -> &str {
78        &self.device_type
79    }
80
81    /// The device type for an infrared remote device.
82    pub fn remote_type(&self) -> &str {
83        &self.remote_type
84    }
85
86    /// [`remote_type()`][Device::remote_type()] if [`is_remote()`][Device::is_remote()],
87    /// otherwise [`device_type()`][Device::device_type()].
88    pub fn device_type_or_remote_type(&self) -> &str {
89        if self.is_remote() {
90            self.remote_type()
91        } else {
92            self.device_type()
93        }
94    }
95
96    /// The parent Hub ID.
97    pub fn hub_device_id(&self) -> &str {
98        &self.hub_device_id
99    }
100
101    fn service(&self) -> anyhow::Result<Arc<SwitchBotService>> {
102        self.service
103            .upgrade()
104            .ok_or_else(|| anyhow::anyhow!("The service is dropped"))
105    }
106
107    pub(crate) fn set_service(&mut self, service: &Arc<SwitchBotService>) {
108        self.service = Arc::downgrade(service);
109    }
110
111    /// Send the `command` to the [SwitchBot API].
112    ///
113    /// Please also see the [`CommandRequest`].
114    ///
115    /// [SwitchBot API]: https://github.com/OpenWonderLabs/SwitchBotAPI
116    ///
117    /// # Examples
118    /// ```no_run
119    /// # use switchbot_api::{CommandRequest, Device};
120    /// # async fn turn_on(device: &Device) -> anyhow::Result<()> {
121    /// let command = CommandRequest { command: "turnOn".into(), ..Default::default() };
122    /// device.command(&command).await?;
123    /// # Ok(())
124    /// # }
125    /// ```
126    pub async fn command(&self, command: &CommandRequest) -> anyhow::Result<()> {
127        if self.is_remote() {
128            // For remote devices, give some delays between commands.
129            self.sleep_for_interval();
130        }
131
132        self.service()?.command(self.device_id(), command).await?;
133
134        if self.is_remote() {
135            self.update_interval();
136        }
137        Ok(())
138    }
139
140    fn sleep_for_interval(&self) {
141        let min_interval = *MIN_INTERVAL_FOR_REMOTE_DEVICES.read().unwrap();
142        let last_command_time = self.last_command_time.read().unwrap();
143        if let Some(last_time) = *last_command_time {
144            let elapsed = last_time.elapsed();
145            if elapsed < min_interval {
146                let duration = min_interval - elapsed;
147                log::debug!("command: sleep {duration:?} for {self}");
148                thread::sleep(duration);
149            }
150        }
151    }
152
153    fn update_interval(&self) {
154        let mut last_command_time = self.last_command_time.write().unwrap();
155        *last_command_time = Some(Instant::now());
156    }
157
158    // pub async fn command_helps(&self) -> anyhow::Result<Vec<CommandHelp>> {
159    //     let mut help = CommandHelp::load().await?;
160    //     if let Some(helps) = help.remove(&self.device_type) {
161    //         return Ok(helps);
162    //     }
163    //     for (key, _) in help {
164    //         println!("{key}");
165    //     }
166    //     Ok(vec![])
167    // }
168
169    /// Get the [device status] from the [SwitchBot API].
170    ///
171    /// Please see [`status_by_key()`][Device::status_by_key()] and some other functions
172    /// to retrieve the status captured by this function.
173    ///
174    /// [SwitchBot API]: https://github.com/OpenWonderLabs/SwitchBotAPI
175    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
176    pub async fn update_status(&self) -> anyhow::Result<()> {
177        let status = self.service()?.status(self.device_id()).await?;
178        if status.is_none() {
179            log::warn!("The query succeeded with no status");
180            return Ok(());
181        }
182        let status = status.unwrap();
183        assert_eq!(self.device_id, status.device_id);
184        let mut writer = self.status.write().unwrap();
185        *writer = status.extra;
186        Ok(())
187    }
188
189    fn status(&self) -> RwLockReadGuard<'_, HashMap<String, serde_json::Value>> {
190        self.status.read().unwrap()
191    }
192
193    /// Get the value of a key from the [device status].
194    ///
195    /// The [`update_status()`][Device::update_status()] must be called prior to this function.
196    ///
197    /// # Examples
198    /// ```no_run
199    /// # use switchbot_api::Device;
200    /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
201    /// device.update_status().await?;
202    /// println!("Power = {}", device.status_by_key("power").unwrap());
203    /// # Ok(())
204    /// # }
205    /// ```
206    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
207    pub fn status_by_key(&self, key: &str) -> Option<serde_json::Value> {
208        self.status().get(key).cloned()
209    }
210
211    /// Evaluate a conditional expression.
212    ///
213    /// Following operators are supported.
214    /// * `key`, `key=true`, and `key=false` for boolean types.
215    /// * `=`, `<`, `<=`, `>`, and `>=` for numeric types.
216    /// * `=` for string and other types.
217    ///
218    /// Returns an error if the expression is invalid,
219    /// or if the `key` does not exist.
220    /// Please also see the [`switchbot-cli` documentation about the
221    /// "if-command"](https://github.com/kojiishi/switchbot-rs/tree/main/cli#if-command).
222    ///
223    /// The [`update_status()`][Device::update_status()] must be called prior to this function.
224    ///
225    /// # Examples
226    /// ```no_run
227    /// # use switchbot_api::Device;
228    /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
229    /// device.update_status().await?;
230    /// println!("Power-on = {}", device.eval_condition("power=on")?);
231    /// # Ok(())
232    /// # }
233    /// ```
234    pub fn eval_condition(&self, condition: &str) -> anyhow::Result<bool> {
235        let condition = ConditionalExpression::try_from(condition)?;
236        let value = self
237            .status_by_key(condition.key)
238            .ok_or_else(|| anyhow::anyhow!(r#"No status key "{}" for {self}"#, condition.key))?;
239        condition.evaluate(&value)
240    }
241
242    /// Write the list of the [device status] to the `writer`.
243    ///
244    /// The [`update_status()`][Device::update_status()] must be called prior to this function.
245    ///
246    /// # Examples
247    /// ```no_run
248    /// # use switchbot_api::Device;
249    /// # async fn print_status(device: &Device) -> anyhow::Result<()> {
250    /// device.update_status().await?;
251    /// device.write_status_to(std::io::stdout());
252    /// # Ok(())
253    /// # }
254    /// ```
255    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
256    pub fn write_status_to(&self, mut writer: impl io::Write) -> io::Result<()> {
257        let status = self.status();
258        for (key, value) in status.iter() {
259            writeln!(writer, "{key}: {value}")?;
260        }
261        Ok(())
262    }
263
264    fn fmt_multi_line(&self, buf: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        writeln!(buf, "Name: {}", self.device_name())?;
266        writeln!(buf, "ID: {}", self.device_id())?;
267        if self.is_remote() {
268            writeln!(buf, "Remote Type: {}", self.remote_type())?;
269        } else {
270            writeln!(buf, "Type: {}", self.device_type())?;
271        }
272        let status = self.status();
273        if !status.is_empty() {
274            writeln!(buf, "Status:")?;
275            for (key, value) in status.iter() {
276                writeln!(buf, "  {key}: {value}")?;
277            }
278        }
279        Ok(())
280    }
281}
282
283impl Display for Device {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        if f.alternate() {
286            return self.fmt_multi_line(f);
287        }
288        write!(
289            f,
290            "{} ({}, ID:{})",
291            self.device_name,
292            if self.is_remote() {
293                self.remote_type()
294            } else {
295                self.device_type()
296            },
297            self.device_id
298        )
299    }
300}