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