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