switchbot_api/
switch_bot_service.rs1use base64::{Engine as _, engine::general_purpose::STANDARD};
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4use std::{
5 sync::Arc,
6 time::{Instant, SystemTime},
7};
8use uuid::Uuid;
9
10use super::*;
11
12#[derive(Debug, Default)]
13pub(crate) struct SwitchBotService {
14 client: reqwest::Client,
15 token: String,
16 secret: String,
17}
18
19impl SwitchBotService {
20 const HOST: &str = "https://api.switch-bot.com";
21
22 pub fn new(token: &str, secret: &str) -> Arc<Self> {
23 Arc::new(SwitchBotService {
24 client: reqwest::Client::new(),
25 token: token.to_string(),
26 secret: secret.to_string(),
27 })
28 }
29
30 pub async fn load_devices(self: &Arc<SwitchBotService>) -> anyhow::Result<DeviceList> {
31 let url = format!("{}/v1.1/devices", Self::HOST);
32 let request = self.client.get(url);
33 let device_list = self.send_as::<DeviceListResponse>(request).await?;
34
35 let mut devices = DeviceList::with_capacity(
36 device_list.device_list.len() + device_list.infrared_remote_list.len(),
37 );
38 devices.extend(device_list.device_list);
39 devices.extend(device_list.infrared_remote_list);
40 for device in devices.iter_mut() {
41 device.set_service(self);
42 }
43 Ok(devices)
44 }
45
46 pub(crate) async fn command(
47 &self,
48 device_id: &str,
49 command: &CommandRequest,
50 ) -> anyhow::Result<()> {
51 let url = format!("{}/v1.1/devices/{device_id}/commands", Self::HOST);
52 let body = serde_json::to_value(command)?;
53 log::debug!("command.request: {body}");
54 let request = self.client.post(url).json(&body);
55 self.send_as_opt(request).await?;
56 Ok(())
57 }
58
59 pub(crate) async fn status(&self, device_id: &str) -> anyhow::Result<Option<Device>> {
60 let url = format!("{}/v1.1/devices/{device_id}/status", Self::HOST);
61 let request = self.client.get(url);
62 let body_json = self.send_as_json(request).await?;
63 if let serde_json::Value::Object(object) = &body_json {
64 if object.is_empty() {
66 return Ok(None);
67 }
68 }
69 let device: Device = serde_json::from_value(body_json)?;
70 Ok(Some(device))
71 }
72
73 async fn send_as<T: serde::de::DeserializeOwned>(
74 &self,
75 request: reqwest::RequestBuilder,
76 ) -> anyhow::Result<T> {
77 let body_json = self.send_as_json(request).await?;
78 let body: T = serde_json::from_value(body_json)?;
79 Ok(body)
80 }
81
82 async fn send_as_json(
83 &self,
84 request: reqwest::RequestBuilder,
85 ) -> anyhow::Result<serde_json::Value> {
86 let body_json = self
87 .send_as_opt(request)
88 .await?
89 .ok_or_else(|| anyhow::anyhow!("Missing `body`"))?;
90 Ok(body_json)
91 }
92
93 async fn send_as_opt(
94 &self,
95 request: reqwest::RequestBuilder,
96 ) -> anyhow::Result<Option<serde_json::Value>> {
97 let start_time = Instant::now();
98 let response = self.add_headers(request)?.send().await?;
99 log::trace!("response: {response:?}");
100 response.error_for_status_ref()?;
101
102 let json: serde_json::Value = response.json().await?;
103 log::trace!("response.json: {json}: elapsed {:?}", start_time.elapsed());
104 Self::body_from_json(json)
105 }
106
107 fn body_from_json(json: serde_json::Value) -> anyhow::Result<Option<serde_json::Value>> {
108 let response: SwitchBotResponse<Option<serde_json::Value>> = serde_json::from_value(json)?;
113
114 if response.status_code != 100 {
117 return Err(SwitchBotError::from(response).into());
118 }
119 Ok(response.body)
120 }
121
122 fn add_headers(
123 &self,
124 builder: reqwest::RequestBuilder,
125 ) -> anyhow::Result<reqwest::RequestBuilder> {
126 let duration_since_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
127 let t = duration_since_epoch.as_millis().to_string();
128 let nonce = Uuid::new_v4().to_string();
129
130 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret.as_bytes())?;
131 mac.update(self.token.as_bytes());
132 mac.update(t.as_bytes());
133 mac.update(nonce.as_bytes());
134 let result = mac.finalize();
135 let sign = STANDARD.encode(result.into_bytes());
136
137 Ok(builder
138 .header("Authorization", self.token.clone())
139 .header("t", t)
140 .header("sign", sign)
141 .header("nonce", nonce))
142 }
143}
144
145#[derive(Debug, serde::Deserialize)]
146#[serde(rename_all = "camelCase")]
147struct SwitchBotResponse<T> {
148 #[allow(dead_code)]
149 pub status_code: u16,
150 #[allow(dead_code)]
151 pub message: String,
152 pub body: T,
153}
154
155#[derive(Debug, serde::Deserialize)]
156#[serde(rename_all = "camelCase")]
157struct DeviceListResponse {
158 device_list: Vec<Device>,
159 infrared_remote_list: Vec<Device>,
160}
161
162#[derive(Debug, thiserror::Error, serde::Deserialize)]
166#[error("SwitchBot API error: {message} ({status_code})")]
167#[serde(rename_all = "camelCase")]
168pub struct SwitchBotError {
169 status_code: u16,
170 message: String,
171}
172
173impl<T> From<SwitchBotResponse<T>> for SwitchBotError {
174 fn from(response: SwitchBotResponse<T>) -> Self {
175 Self {
176 status_code: response.status_code,
177 message: response.message,
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn body_from_json() {
188 let result = SwitchBotService::body_from_json(
189 serde_json::json!({"message":"OK", "statusCode":100, "body":{}}),
190 );
191 assert!(result.is_ok());
192 }
193
194 #[test]
195 fn body_from_json_error() {
196 let result = SwitchBotService::body_from_json(
197 serde_json::json!({"message":"error", "statusCode":500, "body":{}}),
198 );
199 assert!(result.is_err());
200 let error = result.unwrap_err();
201 let switch_bot_error = error.downcast_ref::<SwitchBotError>();
202 assert!(switch_bot_error.is_some());
203 assert_eq!(switch_bot_error.unwrap().status_code, 500);
204 }
205
206 #[test]
207 fn body_from_json_no_body() {
208 let result =
209 SwitchBotService::body_from_json(serde_json::json!({"message":"OK", "statusCode":100}));
210 assert!(result.is_ok());
211 let body = result.unwrap();
212 assert!(body.is_none());
213 }
214
215 #[test]
216 fn error_from_json() -> anyhow::Result<()> {
217 let json_no_body = serde_json::json!(
218 {"message":"unknown command", "statusCode":160});
219 let error: SwitchBotError = serde_json::from_value(json_no_body)?;
220 assert_eq!(error.status_code, 160);
221 assert_eq!(error.message, "unknown command");
222
223 let json_with_body = serde_json::json!(
225 {"message":"unknown command", "statusCode":160, "body":{}});
226 let error: SwitchBotError = serde_json::from_value(json_with_body)?;
227 assert_eq!(error.status_code, 160);
228 assert_eq!(error.message, "unknown command");
229 Ok(())
230 }
231}