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    pub async fn command(&self, command: &CommandRequest) -> anyhow::Result<()> {
96        self.service()?.command(self.device_id(), command).await
97    }
98
99    /// Get the [device status] from the [SwitchBot API].
100    ///
101    /// Please see [`Device::status_by_key()`] and some other functions
102    /// to retrieve the status captured by this function.
103    ///
104    /// [SwitchBot API]: https://github.com/OpenWonderLabs/SwitchBotAPI
105    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
106    pub async fn update_status(&self) -> anyhow::Result<()> {
107        let status = self.service()?.status(self.device_id()).await?;
108        assert_eq!(self.device_id, status.device_id);
109        let mut writer = self.status.write().unwrap();
110        *writer = status.extra;
111        Ok(())
112    }
113
114    fn status(&self) -> RwLockReadGuard<'_, HashMap<String, serde_json::Value>> {
115        self.status.read().unwrap()
116    }
117
118    /// Get the value of a key from the [device status].
119    ///
120    /// The [`Device::update_status()`] must be called prior to this function.
121    ///
122    /// # Examples
123    /// ```no_run
124    /// # use switchbot_api::Device;
125    /// async fn print_power_status(device: &Device) -> anyhow::Result<()> {
126    ///   device.update_status().await?;
127    ///   println!("Power = {}", device.status_by_key("power").unwrap());
128    ///   Ok(())
129    /// }
130    /// ```
131    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
132    pub fn status_by_key(&self, key: &str) -> Option<serde_json::Value> {
133        self.status().get(key).cloned()
134    }
135
136    /// Write the list of the [device status] to the `writer`.
137    ///
138    /// The [`Device::update_status()`] must be called prior to this function.
139    ///
140    /// # Examples
141    /// ```no_run
142    /// # use switchbot_api::Device;
143    /// async fn print_status(device: &Device) -> anyhow::Result<()> {
144    ///   device.update_status().await?;
145    ///   device.write_status_to(std::io::stdout());
146    ///   Ok(())
147    /// }
148    /// ```
149    /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
150    pub fn write_status_to(&self, mut writer: impl io::Write) -> io::Result<()> {
151        let status = self.status();
152        for (key, value) in status.iter() {
153            writeln!(writer, "{key}: {value}")?;
154        }
155        Ok(())
156    }
157
158    fn fmt_multi_line(&self, buf: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        writeln!(buf, "Name: {}", self.device_name())?;
160        writeln!(buf, "ID: {}", self.device_id())?;
161        if self.is_remote() {
162            writeln!(buf, "Remote Type: {}", self.remote_type())?;
163        } else {
164            writeln!(buf, "Type: {}", self.device_type())?;
165        }
166        let status = self.status();
167        if !status.is_empty() {
168            writeln!(buf, "Status:")?;
169            for (key, value) in status.iter() {
170                writeln!(buf, "  {key}: {value}")?;
171            }
172        }
173        Ok(())
174    }
175}
176
177impl Display for Device {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        if f.alternate() {
180            return self.fmt_multi_line(f);
181        }
182        write!(
183            f,
184            "{} ({}, ID:{})",
185            self.device_name,
186            if self.is_remote() {
187                self.remote_type()
188            } else {
189                self.device_type()
190            },
191            self.device_id
192        )
193    }
194}