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 if status.is_none() {
119 log::warn!("The query succeeded with no status");
120 return Ok(());
121 }
122 let status = status.unwrap();
123 assert_eq!(self.device_id, status.device_id);
124 let mut writer = self.status.write().unwrap();
125 *writer = status.extra;
126 Ok(())
127 }
128
129 fn status(&self) -> RwLockReadGuard<'_, HashMap<String, serde_json::Value>> {
130 self.status.read().unwrap()
131 }
132
133 /// Get the value of a key from the [device status].
134 ///
135 /// The [`Device::update_status()`] must be called prior to this function.
136 ///
137 /// # Examples
138 /// ```no_run
139 /// # use switchbot_api::Device;
140 /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
141 /// device.update_status().await?;
142 /// println!("Power = {}", device.status_by_key("power").unwrap());
143 /// # Ok(())
144 /// # }
145 /// ```
146 /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
147 pub fn status_by_key(&self, key: &str) -> Option<serde_json::Value> {
148 self.status().get(key).cloned()
149 }
150
151 /// Evaluate a conditional expression.
152 ///
153 /// Following operators are supported.
154 /// * `key`, `key=true`, and `key=false` for boolean types.
155 /// * `=`, `<`, `<=`, `>`, and `>=` for numeric types.
156 /// * `=` for string and other types.
157 ///
158 /// Returns an error if the expression is invalid,
159 /// or if the `key` does not exist.
160 /// Please also see the [`switchbot-cli` documentation about the
161 /// "if-command"](https://github.com/kojiishi/switchbot-rs/tree/main/cli#if-command).
162 ///
163 /// The [`Device::update_status()`] must be called prior to this function.
164 ///
165 /// # Examples
166 /// ```no_run
167 /// # use switchbot_api::Device;
168 /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
169 /// device.update_status().await?;
170 /// println!("Power-on = {}", device.eval_condition("power=on")?);
171 /// # Ok(())
172 /// # }
173 /// ```
174 pub fn eval_condition(&self, condition: &str) -> anyhow::Result<bool> {
175 let condition = ConditionalExpression::try_from(condition)?;
176 let value = self
177 .status_by_key(condition.key)
178 .ok_or_else(|| anyhow::anyhow!(r#"No status key "{}" for {self}"#, condition.key))?;
179 condition.evaluate(&value)
180 }
181
182 /// Write the list of the [device status] to the `writer`.
183 ///
184 /// The [`Device::update_status()`] must be called prior to this function.
185 ///
186 /// # Examples
187 /// ```no_run
188 /// # use switchbot_api::Device;
189 /// # async fn print_status(device: &Device) -> anyhow::Result<()> {
190 /// device.update_status().await?;
191 /// device.write_status_to(std::io::stdout());
192 /// # Ok(())
193 /// # }
194 /// ```
195 /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
196 pub fn write_status_to(&self, mut writer: impl io::Write) -> io::Result<()> {
197 let status = self.status();
198 for (key, value) in status.iter() {
199 writeln!(writer, "{key}: {value}")?;
200 }
201 Ok(())
202 }
203
204 fn fmt_multi_line(&self, buf: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 writeln!(buf, "Name: {}", self.device_name())?;
206 writeln!(buf, "ID: {}", self.device_id())?;
207 if self.is_remote() {
208 writeln!(buf, "Remote Type: {}", self.remote_type())?;
209 } else {
210 writeln!(buf, "Type: {}", self.device_type())?;
211 }
212 let status = self.status();
213 if !status.is_empty() {
214 writeln!(buf, "Status:")?;
215 for (key, value) in status.iter() {
216 writeln!(buf, " {key}: {value}")?;
217 }
218 }
219 Ok(())
220 }
221}
222
223impl Display for Device {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 if f.alternate() {
226 return self.fmt_multi_line(f);
227 }
228 write!(
229 f,
230 "{} ({}, ID:{})",
231 self.device_name,
232 if self.is_remote() {
233 self.remote_type()
234 } else {
235 self.device_type()
236 },
237 self.device_id
238 )
239 }
240}