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}