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 start_time = Instant::now();
32 let url = format!("{}/v1.1/devices", Self::HOST);
33 let json: serde_json::Value = self
34 .add_headers(self.client.get(url))?
35 .send()
36 .await?
37 .json()
38 .await?;
39 log::trace!(
40 "devices.json: {json:#?}: elapsed {:?}",
41 start_time.elapsed()
42 );
43
44 let response: SwitchBotResponse<DeviceListResponse> = serde_json::from_value(json)?;
45 let mut devices = DeviceList::with_capacity(
47 response.body.device_list.len() + response.body.infrared_remote_list.len(),
48 );
49 devices.extend(response.body.device_list);
50 devices.extend(response.body.infrared_remote_list);
51 for device in devices.iter_mut() {
52 device.set_service(self);
53 }
54 Ok(devices)
55 }
56
57 pub(crate) async fn command(
58 &self,
59 device_id: &str,
60 command: &CommandRequest,
61 ) -> anyhow::Result<()> {
62 let start_time = Instant::now();
63 let url = format!("{}/v1.1/devices/{device_id}/commands", Self::HOST);
64 let body = serde_json::to_value(command)?;
65 log::debug!("command.request: {body}");
66 let json: serde_json::Value = self
67 .add_headers(self.client.post(url))?
68 .json(&body)
69 .send()
70 .await?
71 .json()
72 .await?;
73 log::trace!(
74 "command.response: {json}: elapsed {:?}",
75 start_time.elapsed()
76 );
77
78 let response: SwitchBotError = serde_json::from_value(json)?;
79 if response.status_code != 100 {
82 return Err(response.into());
83 }
84 Ok(())
85 }
86
87 fn add_headers(
88 &self,
89 builder: reqwest::RequestBuilder,
90 ) -> anyhow::Result<reqwest::RequestBuilder> {
91 let duration_since_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
92 let t = duration_since_epoch.as_millis().to_string();
93 let nonce = Uuid::new_v4().to_string();
94
95 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret.as_bytes())?;
96 mac.update(self.token.as_bytes());
97 mac.update(t.as_bytes());
98 mac.update(nonce.as_bytes());
99 let result = mac.finalize();
100 let sign = STANDARD.encode(result.into_bytes());
101
102 Ok(builder
103 .header("Authorization", self.token.clone())
104 .header("t", t)
105 .header("sign", sign)
106 .header("nonce", nonce))
107 }
108}
109
110#[derive(Debug, serde::Deserialize)]
111#[serde(rename_all = "camelCase")]
112struct SwitchBotResponse<T> {
113 #[allow(dead_code)]
114 pub status_code: u16,
115 #[allow(dead_code)]
116 pub message: String,
117 pub body: T,
118}
119
120#[derive(Debug, serde::Deserialize)]
121#[serde(rename_all = "camelCase")]
122struct DeviceListResponse {
123 device_list: Vec<Device>,
124 infrared_remote_list: Vec<Device>,
125}
126
127#[derive(Debug, thiserror::Error, serde::Deserialize)]
131#[error("SwitchBot API error: {message} ({status_code})")]
132#[serde(rename_all = "camelCase")]
133pub struct SwitchBotError {
134 status_code: u16,
135 message: String,
136}
137
138impl<T> From<SwitchBotResponse<T>> for SwitchBotError {
139 fn from(response: SwitchBotResponse<T>) -> Self {
140 Self {
141 status_code: response.status_code,
142 message: response.message,
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn error_from_json() -> anyhow::Result<()> {
153 let json_no_body = serde_json::json!(
154 {"message":"unknown command", "statusCode":160});
155 let error: SwitchBotError = serde_json::from_value(json_no_body)?;
156 assert_eq!(error.status_code, 160);
157 assert_eq!(error.message, "unknown command");
158
159 let json_with_body = serde_json::json!(
161 {"message":"unknown command", "statusCode":160, "body":{}});
162 let error: SwitchBotError = serde_json::from_value(json_with_body)?;
163 assert_eq!(error.status_code, 160);
164 assert_eq!(error.message, "unknown command");
165 Ok(())
166 }
167}