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 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()
34 .await?
35 .json()
36 .await?;
37 log::trace!("devices.json: {json:#?}");
38 let response: SwitchBotResponse<DeviceListResponse> = serde_json::from_value(json)?;
39 let mut devices = response.body.deviceList;
41 devices.extend(response.body.infraredRemoteList);
42 for device in devices.iter_mut() {
43 device.set_service(Rc::clone(self));
44 }
45 Ok(devices)
46 }
47
48 pub(crate) async fn command(
49 &self,
50 device_id: &str,
51 command: &CommandRequest,
52 ) -> anyhow::Result<()> {
53 let url = format!("{}/v1.1/devices/{device_id}/commands", Self::HOST);
54 let body = serde_json::to_value(command)?;
55 log::debug!("command.request: {body}");
56 let json: serde_json::Value = self
57 .add_headers(self.client.post(url))?
58 .header("Content-Type", "application/json")
59 .json(&body)
60 .send()
61 .await?
62 .json()
63 .await?;
64
65 log::trace!("command.response: {json}");
66 let response: SwitchBotError = serde_json::from_value(json)?;
67 if response.status_code != 100 {
70 return Err(response.into());
71 }
72 Ok(())
73 }
74
75 fn add_headers(
76 &self,
77 builder: reqwest::RequestBuilder,
78 ) -> anyhow::Result<reqwest::RequestBuilder> {
79 let duration_since_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
80 let t = duration_since_epoch.as_millis().to_string();
81 let nonce = Uuid::new_v4().to_string();
82
83 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret.as_bytes())?;
84 mac.update(self.token.as_bytes());
85 mac.update(t.as_bytes());
86 mac.update(nonce.as_bytes());
87 let result = mac.finalize();
88 let sign = STANDARD.encode(result.into_bytes());
89
90 Ok(builder
91 .header("Authorization", self.token.clone())
92 .header("t", t)
93 .header("sign", sign)
94 .header("nonce", nonce))
95 }
96}
97
98#[derive(Debug, serde::Deserialize)]
99#[allow(non_snake_case)]
100pub struct SwitchBotResponse<T> {
101 #[allow(dead_code)]
102 pub statusCode: u16,
103 #[allow(dead_code)]
104 pub message: String,
105 pub body: T,
106}
107
108#[derive(Debug, serde::Deserialize)]
109#[allow(non_snake_case)]
110pub struct DeviceListResponse {
111 pub deviceList: DeviceList,
112 pub infraredRemoteList: DeviceList,
113}
114
115#[derive(Debug, serde::Serialize)]
132pub struct CommandRequest {
133 pub command: String,
135 pub parameter: String,
138 #[serde(rename = "commandType")]
141 pub command_type: String,
142}
143
144impl Default for CommandRequest {
145 fn default() -> Self {
146 Self {
147 command: String::default(),
148 parameter: "default".into(),
149 command_type: "command".into(),
150 }
151 }
152}
153
154#[derive(Debug, thiserror::Error, serde::Deserialize)]
158#[error("SwitchBot API error: {message} ({status_code})")]
159pub struct SwitchBotError {
160 #[serde(rename = "statusCode")]
161 status_code: u16,
162 message: String,
163}
164
165impl<T> From<SwitchBotResponse<T>> for SwitchBotError {
166 fn from(response: SwitchBotResponse<T>) -> Self {
167 Self {
168 status_code: response.statusCode,
169 message: response.message,
170 }
171 }
172}