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::<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_opt(request).await?;
56 Ok(())
57 }
58
59 pub(crate) async fn status(&self, device_id: &str) -> anyhow::Result<Device> {
60 let url = format!("{}/v1.1/devices/{device_id}/status", Self::HOST);
61 let request = self.client.get(url);
62 let device = self.send::<Device>(request).await?;
63 Ok(device)
64 }
65
66 async fn send<T: serde::de::DeserializeOwned>(
67 &self,
68 request: reqwest::RequestBuilder,
69 ) -> anyhow::Result<T> {
70 let body_json = self
71 .send_opt(request)
72 .await?
73 .ok_or_else(|| anyhow::anyhow!("Missing `body`"))?;
74 let body: T = serde_json::from_value(body_json)?;
75 Ok(body)
76 }
77
78 async fn send_opt(
79 &self,
80 request: reqwest::RequestBuilder,
81 ) -> anyhow::Result<Option<serde_json::Value>> {
82 let start_time = Instant::now();
83 let response = self.add_headers(request)?.send().await?;
84 log::trace!("response: {response:?}");
85 response.error_for_status_ref()?;
86
87 let json: serde_json::Value = response.json().await?;
88 log::trace!("response.json: {json}: elapsed {:?}", start_time.elapsed());
89 Self::body_from_json(json)
90 }
91
92 fn body_from_json(json: serde_json::Value) -> anyhow::Result<Option<serde_json::Value>> {
93 let response: SwitchBotResponse<Option<serde_json::Value>> = serde_json::from_value(json)?;
98
99 if response.status_code != 100 {
102 return Err(SwitchBotError::from(response).into());
103 }
104 Ok(response.body)
105 }
106
107 fn add_headers(
108 &self,
109 builder: reqwest::RequestBuilder,
110 ) -> anyhow::Result<reqwest::RequestBuilder> {
111 let duration_since_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
112 let t = duration_since_epoch.as_millis().to_string();
113 let nonce = Uuid::new_v4().to_string();
114
115 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret.as_bytes())?;
116 mac.update(self.token.as_bytes());
117 mac.update(t.as_bytes());
118 mac.update(nonce.as_bytes());
119 let result = mac.finalize();
120 let sign = STANDARD.encode(result.into_bytes());
121
122 Ok(builder
123 .header("Authorization", self.token.clone())
124 .header("t", t)
125 .header("sign", sign)
126 .header("nonce", nonce))
127 }
128}
129
130#[derive(Debug, serde::Deserialize)]
131#[serde(rename_all = "camelCase")]
132struct SwitchBotResponse<T> {
133 #[allow(dead_code)]
134 pub status_code: u16,
135 #[allow(dead_code)]
136 pub message: String,
137 pub body: T,
138}
139
140#[derive(Debug, serde::Deserialize)]
141#[serde(rename_all = "camelCase")]
142struct DeviceListResponse {
143 device_list: Vec<Device>,
144 infrared_remote_list: Vec<Device>,
145}
146
147#[derive(Debug, thiserror::Error, serde::Deserialize)]
151#[error("SwitchBot API error: {message} ({status_code})")]
152#[serde(rename_all = "camelCase")]
153pub struct SwitchBotError {
154 status_code: u16,
155 message: String,
156}
157
158impl<T> From<SwitchBotResponse<T>> for SwitchBotError {
159 fn from(response: SwitchBotResponse<T>) -> Self {
160 Self {
161 status_code: response.status_code,
162 message: response.message,
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn body_from_json() {
173 let result = SwitchBotService::body_from_json(
174 serde_json::json!({"message":"OK", "statusCode":100, "body":{}}),
175 );
176 assert!(result.is_ok());
177 }
178
179 #[test]
180 fn body_from_json_error() {
181 let result = SwitchBotService::body_from_json(
182 serde_json::json!({"message":"error", "statusCode":500, "body":{}}),
183 );
184 assert!(result.is_err());
185 let error = result.unwrap_err();
186 let switch_bot_error = error.downcast_ref::<SwitchBotError>();
187 assert!(switch_bot_error.is_some());
188 assert_eq!(switch_bot_error.unwrap().status_code, 500);
189 }
190
191 #[test]
192 fn body_from_json_no_body() {
193 let result =
194 SwitchBotService::body_from_json(serde_json::json!({"message":"OK", "statusCode":100}));
195 assert!(result.is_ok());
196 let body = result.unwrap();
197 assert!(body.is_none());
198 }
199
200 #[test]
201 fn error_from_json() -> anyhow::Result<()> {
202 let json_no_body = serde_json::json!(
203 {"message":"unknown command", "statusCode":160});
204 let error: SwitchBotError = serde_json::from_value(json_no_body)?;
205 assert_eq!(error.status_code, 160);
206 assert_eq!(error.message, "unknown command");
207
208 let json_with_body = serde_json::json!(
210 {"message":"unknown command", "statusCode":160, "body":{}});
211 let error: SwitchBotError = serde_json::from_value(json_with_body)?;
212 assert_eq!(error.status_code, 160);
213 assert_eq!(error.message, "unknown command");
214 Ok(())
215 }
216}