switchbot_api/
device.rs

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