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        assert_eq!(self.device_id, status.device_id);
119        let mut writer = self.status.write().unwrap();
120        *writer = status.extra;
121        Ok(())
122    }
123
124    fn status(&self) -> RwLockReadGuard<'_, HashMap<String, serde_json::Value>> {
125        self.status.read().unwrap()
126    }
127
128    /// Get the value of a key from the [device status].
129    ///
130    /// The [`Device::update_status()`] must be called prior to this function.
131    ///
132    /// # Examples
133    /// ```no_run
134    /// # use switchbot_api::Device;
135    /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
136    /// device.update_status().await?;
137    /// println!("Power = {}", device.status_by_key("power").unwrap());
138    /// # Ok(())
139    /// # }
140    /// ```
141    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
142    pub fn status_by_key(&self, key: &str) -> Option<serde_json::Value> {
143        self.status().get(key).cloned()
144    }
145
146    /// Evaluate a conditional expression.
147    ///
148    /// The expression should be in the form of `key=value`.
149    /// When the value is a boolean type, `key` is also a valid expression.
150    ///
151    /// Returns an error if the expression is invalid,
152    /// or if the `key` does not exist.
153    ///
154    /// The [`Device::update_status()`] must be called prior to this function.
155    ///
156    /// # Examples
157    /// ```no_run
158    /// # use switchbot_api::Device;
159    /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
160    /// device.update_status().await?;
161    /// println!("Power-on = {}", device.eval_condition("power=on")?);
162    /// # Ok(())
163    /// # }
164    /// ```
165    pub fn eval_condition(&self, condition: &str) -> anyhow::Result<bool> {
166        let condition = ConditionalExpression::try_from(condition)?;
167        let value = self
168            .status_by_key(condition.key)
169            .ok_or_else(|| anyhow::anyhow!(r#"No status key "{}" for {self}"#, condition.key))?;
170        condition.evaluate(&value)
171    }
172
173    /// Write the list of the [device status] to the `writer`.
174    ///
175    /// The [`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_status(device: &Device) -> anyhow::Result<()> {
181    /// device.update_status().await?;
182    /// device.write_status_to(std::io::stdout());
183    /// # Ok(())
184    /// # }
185    /// ```
186    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
187    pub fn write_status_to(&self, mut writer: impl io::Write) -> io::Result<()> {
188        let status = self.status();
189        for (key, value) in status.iter() {
190            writeln!(writer, "{key}: {value}")?;
191        }
192        Ok(())
193    }
194
195    fn fmt_multi_line(&self, buf: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        writeln!(buf, "Name: {}", self.device_name())?;
197        writeln!(buf, "ID: {}", self.device_id())?;
198        if self.is_remote() {
199            writeln!(buf, "Remote Type: {}", self.remote_type())?;
200        } else {
201            writeln!(buf, "Type: {}", self.device_type())?;
202        }
203        let status = self.status();
204        if !status.is_empty() {
205            writeln!(buf, "Status:")?;
206            for (key, value) in status.iter() {
207                writeln!(buf, "  {key}: {value}")?;
208            }
209        }
210        Ok(())
211    }
212}
213
214impl Display for Device {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        if f.alternate() {
217            return self.fmt_multi_line(f);
218        }
219        write!(
220            f,
221            "{} ({}, ID:{})",
222            self.device_name,
223            if self.is_remote() {
224                self.remote_type()
225            } else {
226                self.device_type()
227            },
228            self.device_id
229        )
230    }
231}