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 condition expression in the form of "key" or "key=value".
147 ///
148 /// The [`Device::update_status()`] must be called prior to this function.
149 ///
150 /// # Examples
151 /// ```no_run
152 /// # use switchbot_api::Device;
153 /// # async fn print_power_status(device: &Device) -> anyhow::Result<()> {
154 /// device.update_status().await?;
155 /// println!("Power-on = {}", device.eval_condition("power=on")?);
156 /// # Ok(())
157 /// # }
158 /// ```
159 pub fn eval_condition(&self, condition: &str) -> anyhow::Result<bool> {
160 let condition = ConditionExpression::try_from(condition)?;
161 let value = self
162 .status_by_key(condition.key)
163 .ok_or_else(|| anyhow::anyhow!(r#"No status key "{}" for {self}"#, condition.key))?;
164 condition.evaluate(&value)
165 }
166
167 /// Write the list of the [device status] to the `writer`.
168 ///
169 /// The [`Device::update_status()`] must be called prior to this function.
170 ///
171 /// # Examples
172 /// ```no_run
173 /// # use switchbot_api::Device;
174 /// # async fn print_status(device: &Device) -> anyhow::Result<()> {
175 /// device.update_status().await?;
176 /// device.write_status_to(std::io::stdout());
177 /// # Ok(())
178 /// # }
179 /// ```
180 /// [device status]: https://github.com/OpenWonderLabs/SwitchBotAPI#get-device-status
181 pub fn write_status_to(&self, mut writer: impl io::Write) -> io::Result<()> {
182 let status = self.status();
183 for (key, value) in status.iter() {
184 writeln!(writer, "{key}: {value}")?;
185 }
186 Ok(())
187 }
188
189 fn fmt_multi_line(&self, buf: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 writeln!(buf, "Name: {}", self.device_name())?;
191 writeln!(buf, "ID: {}", self.device_id())?;
192 if self.is_remote() {
193 writeln!(buf, "Remote Type: {}", self.remote_type())?;
194 } else {
195 writeln!(buf, "Type: {}", self.device_type())?;
196 }
197 let status = self.status();
198 if !status.is_empty() {
199 writeln!(buf, "Status:")?;
200 for (key, value) in status.iter() {
201 writeln!(buf, " {key}: {value}")?;
202 }
203 }
204 Ok(())
205 }
206}
207
208impl Display for Device {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 if f.alternate() {
211 return self.fmt_multi_line(f);
212 }
213 write!(
214 f,
215 "{} ({}, ID:{})",
216 self.device_name,
217 if self.is_remote() {
218 self.remote_type()
219 } else {
220 self.device_type()
221 },
222 self.device_id
223 )
224 }
225}