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