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 condition expression in the form of "key" or "key=value".
147    ///
148    /// The [`Device::update_status()`] must be called prior to this function.
149    ///
150    /// # Examples
151    /// ```no_run
152    /// # use switchbot_api::Device;
153    /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
154    /// device.update_status().await?;
155    /// println!("Power-on = {}", device.eval_condition("power=on")?);
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub fn eval_condition(&self, condition: &str) -> anyhow::Result<bool> {
160        let condition = ConditionExpression::try_from(condition)?;
161        let value = self
162            .status_by_key(condition.key)
163            .ok_or_else(|| anyhow::anyhow!(r#"No status key "{}" for {self}"#, condition.key))?;
164        condition.evaluate(&value)
165    }
166
167    /// Write the list of the [device status] to the `writer`.
168    ///
169    /// The [`Device::update_status()`] must be called prior to this function.
170    ///
171    /// # Examples
172    /// ```no_run
173    /// # use switchbot_api::Device;
174    /// # async fn print_status(device: &Device) -> anyhow::Result<()> {
175    /// device.update_status().await?;
176    /// device.write_status_to(std::io::stdout());
177    /// # Ok(())
178    /// # }
179    /// ```
180    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
181    pub fn write_status_to(&self, mut writer: impl io::Write) -> io::Result<()> {
182        let status = self.status();
183        for (key, value) in status.iter() {
184            writeln!(writer, "{key}: {value}")?;
185        }
186        Ok(())
187    }
188
189    fn fmt_multi_line(&self, buf: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        writeln!(buf, "Name: {}", self.device_name())?;
191        writeln!(buf, "ID: {}", self.device_id())?;
192        if self.is_remote() {
193            writeln!(buf, "Remote Type: {}", self.remote_type())?;
194        } else {
195            writeln!(buf, "Type: {}", self.device_type())?;
196        }
197        let status = self.status();
198        if !status.is_empty() {
199            writeln!(buf, "Status:")?;
200            for (key, value) in status.iter() {
201                writeln!(buf, "  {key}: {value}")?;
202            }
203        }
204        Ok(())
205    }
206}
207
208impl Display for Device {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        if f.alternate() {
211            return self.fmt_multi_line(f);
212        }
213        write!(
214            f,
215            "{} ({}, ID:{})",
216            self.device_name,
217            if self.is_remote() {
218                self.remote_type()
219            } else {
220                self.device_type()
221            },
222            self.device_id
223        )
224    }
225}