switchbot_api/
switch_bot_service.rs1use base64::{Engine as _, engine::general_purpose::STANDARD};
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4use std::{rc::Rc, time::SystemTime};
5use uuid::Uuid;
6
7use super::*;
8
9#[derive(Debug, Default)]
10pub(crate) struct SwitchBotService {
11 client: reqwest::Client,
12 token: String,
13 secret: String,
14}
15
16impl SwitchBotService {
17 const HOST: &str = "https://api.switch-bot.com";
18
19 pub fn new(token: &str, secret: &str) -> Rc<Self> {
20 Rc::new(SwitchBotService {
21 client: reqwest::Client::new(),
22 token: token.to_string(),
23 secret: secret.to_string(),
24 })
25 }
26
27 pub async fn load_devices(self: &Rc<SwitchBotService>) -> anyhow::Result<DeviceList> {
28 let url = format!("{}/v1.1/devices", Self::HOST);
29 let json: serde_json::Value = self
31 .add_headers(self.client.get(url))?
32 .send()
33 .await?
34 .json()
35 .await?;
36 log::trace!("devices.json: {json:#?}");
37 let response: SwitchBotResponse<DeviceListResponse> = serde_json::from_value(json)?;
38 let mut devices = DeviceList::with_capacity(
40 response.body.device_list.len() + response.body.infrared_remote_list.len(),
41 );
42 devices.extend(response.body.device_list);
43 devices.extend(response.body.infrared_remote_list);
44 for device in devices.iter_mut() {
45 device.set_service(self);
46 }
47 Ok(devices)
48 }
49
50 pub(crate) async fn command(
51 &self,
52 device_id: &str,
53 command: &CommandRequest,
54 ) -> anyhow::Result<()> {
55 let url = format!("{}/v1.1/devices/{device_id}/commands", Self::HOST);
56 let body = serde_json::to_value(command)?;
57 log::debug!("command.request: {body}");
58 let json: serde_json::Value = self
59 .add_headers(self.client.post(url))?
60 .json(&body)
61 .send()
62 .await?
63 .json()
64 .await?;
65
66 log::trace!("command.response: {json}");
67 let response: SwitchBotError = serde_json::from_value(json)?;
68 if response.status_code != 100 {
71 return Err(response.into());
72 }
73 Ok(())
74 }
75
76 fn add_headers(
77 &self,
78 builder: reqwest::RequestBuilder,
79 ) -> anyhow::Result<reqwest::RequestBuilder> {
80 let duration_since_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
81 let t = duration_since_epoch.as_millis().to_string();
82 let nonce = Uuid::new_v4().to_string();
83
84 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret.as_bytes())?;
85 mac.update(self.token.as_bytes());
86 mac.update(t.as_bytes());
87 mac.update(nonce.as_bytes());
88 let result = mac.finalize();
89 let sign = STANDARD.encode(result.into_bytes());
90
91 Ok(builder
92 .header("Authorization", self.token.clone())
93 .header("t", t)
94 .header("sign", sign)
95 .header("nonce", nonce))
96 }
97}
98
99#[derive(Debug, serde::Deserialize)]
100#[serde(rename_all = "camelCase")]
101struct SwitchBotResponse<T> {
102 #[allow(dead_code)]
103 pub status_code: u16,
104 #[allow(dead_code)]
105 pub message: String,
106 pub body: T,
107}
108
109#[derive(Debug, serde::Deserialize)]
110#[serde(rename_all = "camelCase")]
111struct DeviceListResponse {
112 device_list: Vec<Device>,
113 infrared_remote_list: Vec<Device>,
114}
115
116#[derive(Debug, serde::Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct CommandRequest {
135 pub command: String,
137 pub parameter: String,
140 pub command_type: String,
143}
144
145impl Default for CommandRequest {
146 fn default() -> Self {
147 Self {
148 command: String::default(),
149 parameter: "default".into(),
150 command_type: "command".into(),
151 }
152 }
153}
154
155#[derive(Debug, thiserror::Error, serde::Deserialize)]
159#[error("SwitchBot API error: {message} ({status_code})")]
160#[serde(rename_all = "camelCase")]
161pub struct SwitchBotError {
162 status_code: u16,
163 message: String,
164}
165
166impl<T> From<SwitchBotResponse<T>> for SwitchBotError {
167 fn from(response: SwitchBotResponse<T>) -> Self {
168 Self {
169 status_code: response.status_code,
170 message: response.message,
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn error_from_json() -> anyhow::Result<()> {
181 let json_no_body = serde_json::json!(
182 {"message":"unknown command", "statusCode":160});
183 let error: SwitchBotError = serde_json::from_value(json_no_body)?;
184 assert_eq!(error.status_code, 160);
185 assert_eq!(error.message, "unknown command");
186
187 let json_with_body = serde_json::json!(
189 {"message":"unknown command", "statusCode":160, "body":{}});
190 let error: SwitchBotError = serde_json::from_value(json_with_body)?;
191 assert_eq!(error.status_code, 160);
192 assert_eq!(error.message, "unknown command");
193 Ok(())
194 }
195}