├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── Cargo.toml ├── readme.md └── src ├── errors.rs ├── lib.rs ├── native_app.rs ├── rest.rs └── types.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v2 15 | 16 | - uses: actions/cache@v1 17 | with: 18 | path: $HOME/.cargo 19 | key: cargo-${{ hashFiles('Cargo.lock') }} 20 | 21 | - name: Install stable toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: stable 26 | override: true 27 | components: rustfmt, clippy 28 | 29 | - name: Build 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: build 33 | args: --release 34 | 35 | - name: Test 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: test 39 | 40 | - name: Run cargo clippy 41 | uses: actions-rs/cargo@v1 42 | with: 43 | command: clippy 44 | 45 | - name: Run cargo fmt 46 | uses: actions-rs/cargo@v1 47 | with: 48 | command: fmt 49 | args: --all -- --check 50 | 51 | - name: Check 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: check 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "chrono", 4 | "reqwest" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "cargo", 6 | "subcommand": "build", 7 | "problemMatcher": [ 8 | "$rustc" 9 | ], 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "label": "Rust: cargo build - halcyon" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "homeassistant" 3 | description = "This is a library for connecting to the Home Assistant API" 4 | version = "0.3.0" 5 | authors = ["Bradley Nelson "] 6 | edition = "2018" 7 | readme = "readme.md" 8 | homepage = "https://api.halcyon.casa" 9 | repository = "https://github.com/selfhostedshow/homeassistant-rs" 10 | license = "Apache-2.0" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | futures = "0.3" 16 | reqwest = { version = "0.10", features = ["json"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | chrono = "0.4" 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Home Assistent API 2 | 3 | This is a Library for connecting to the home assistent api 4 | 5 | This is a very early build and the api will change 6 | 7 | ## Building 8 | 9 | ### Prerequisites 10 | 11 | * Rust 12 | 13 | To build Halcyon, run `cargo build`. -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug)] 4 | pub enum Error { 5 | Request(reqwest::Error), 6 | HaApi(String), 7 | Config(String), 8 | Refresh(), 9 | NoAuth(), 10 | PoisonError( 11 | std::sync::PoisonError< 12 | std::sync::RwLockReadGuard<'static, std::option::Option>, 13 | >, 14 | ), 15 | } 16 | 17 | impl From for Error { 18 | fn from(error: reqwest::Error) -> Self { 19 | Error::Request(error) 20 | } 21 | } 22 | 23 | impl 24 | From< 25 | std::sync::PoisonError< 26 | std::sync::RwLockReadGuard<'static, std::option::Option>, 27 | >, 28 | > for Error 29 | { 30 | fn from( 31 | error: std::sync::PoisonError< 32 | std::sync::RwLockReadGuard<'static, std::option::Option>, 33 | >, 34 | ) -> Self { 35 | Error::PoisonError(error) 36 | } 37 | } 38 | 39 | impl fmt::Display for Error { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | match self { 42 | Error::Request(inner) => write!(f, "{}", inner), 43 | Error::Config(inner) => write!(f, "{}", inner), 44 | Error::HaApi(inner) => write!(f, "{}", inner), 45 | Error::PoisonError(inner) => write!(f, "{}", inner), 46 | Error::Refresh() => write!(f, "Tried to refresh a long lived access token"), 47 | Error::NoAuth() => write!(f, "There are no Authentication Credentials"), 48 | } 49 | } 50 | } 51 | 52 | impl std::error::Error for Error { 53 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 54 | match self { 55 | Error::Request(inner) => Some(inner), 56 | _ => None, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::types::*; 2 | use std::convert::TryFrom; 3 | use std::sync::{Arc, RwLock, Weak}; 4 | use std::time; 5 | 6 | pub mod errors; 7 | pub mod native_app; 8 | pub mod rest; 9 | pub mod types; 10 | 11 | #[derive(Debug)] 12 | pub struct HomeAssistantAPI { 13 | instance_url: String, 14 | token: Token, 15 | client_id: String, 16 | self_reference: Weak>, 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub enum Token { 21 | Oauth(OAuthToken), 22 | LongLived(LongLivedToken), 23 | None, 24 | } 25 | 26 | impl Token { 27 | pub fn as_string(&self) -> Result { 28 | match self { 29 | Token::Oauth(token) => Ok(token.token.clone()), 30 | Token::LongLived(token) => Ok(token.token.clone()), 31 | Token::None => Err(errors::Error::NoAuth()), 32 | } 33 | } 34 | 35 | pub fn need_refresh(&self) -> bool { 36 | match self { 37 | Token::Oauth(token) => { 38 | match time::SystemTime::now().duration_since(token.token_expiration) { 39 | Ok(sec_left) => sec_left > time::Duration::from_secs(10), 40 | Err(_) => false, 41 | } 42 | } 43 | Token::LongLived(_) => false, 44 | Token::None => false, 45 | } 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct OAuthToken { 51 | token: String, 52 | token_expiration: std::time::SystemTime, 53 | refresh_token: String, 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | pub struct LongLivedToken { 58 | token: String, 59 | } 60 | 61 | impl HomeAssistantAPI { 62 | pub fn new(instance_url: String, client_id: String) -> Arc> { 63 | let token = Token::None; 64 | let ret = Arc::new(RwLock::new(Self { 65 | instance_url, 66 | token, 67 | client_id, 68 | self_reference: Weak::new(), 69 | })); 70 | 71 | ret.write().unwrap().self_reference = Arc::downgrade(&ret); 72 | 73 | ret 74 | } 75 | 76 | pub fn set_oauth_token( 77 | &mut self, 78 | access_token: String, 79 | expires_in: u32, 80 | refresh_token: String, 81 | ) { 82 | let oauth = OAuthToken { 83 | token: access_token, 84 | token_expiration: time::SystemTime::now() 85 | + time::Duration::from_secs(expires_in as u64), 86 | refresh_token, 87 | }; 88 | self.token = Token::Oauth(oauth); 89 | } 90 | 91 | pub fn set_long_lived_token(&mut self, token: String) { 92 | self.token = Token::LongLived(LongLivedToken { token }); 93 | } 94 | 95 | pub async fn refresh_oauth_token(&mut self) -> Result<(), errors::Error> { 96 | let refresh_token = String::from("test"); 97 | 98 | let response = reqwest::Client::new() 99 | .post(format!("{}/auth/token", self.instance_url).as_str()) 100 | .query(&[ 101 | ("grant_type", "refresh_token"), 102 | ("client_id", &self.client_id), 103 | ("refresh_token", refresh_token.as_str()), 104 | ]) 105 | .send() 106 | .await?; 107 | 108 | let refresh_token_resp: RefreshAccessTokenResponse = response.json().await?; 109 | self.set_oauth_token( 110 | refresh_token_resp.access_token, 111 | refresh_token_resp.expires_in, 112 | refresh_token, 113 | ); 114 | Ok(()) 115 | } 116 | 117 | pub async fn access_token( 118 | &mut self, 119 | code: String, 120 | client_id: String, 121 | ) -> Result { 122 | let request = GetAccessTokenRequest { 123 | grant_type: "authorization_code".to_string(), 124 | code, 125 | client_id, 126 | }; 127 | let resp = reqwest::Client::new() 128 | .post(format!("{}/auth/token", self.instance_url).as_str()) 129 | .form(&request) 130 | .send() 131 | .await?; 132 | 133 | match resp.status().as_str() { 134 | "200" => { 135 | let access_token_resp = resp.json::().await?; 136 | self.set_oauth_token( 137 | access_token_resp.access_token.clone(), 138 | access_token_resp.expires_in, 139 | access_token_resp.refresh_token.clone(), 140 | ); 141 | Ok(access_token_resp) 142 | } 143 | _ => { 144 | let error = resp.json::().await?; 145 | Err(errors::Error::HaApi(format!( 146 | "Error getting access token from HA Error: {} Details: {}", 147 | error.error, error.error_description 148 | ))) 149 | } 150 | } 151 | } 152 | 153 | pub async fn get_rest_client(&self) -> rest::Rest { 154 | match rest::Rest::try_from(self.self_reference.clone()) { 155 | Ok(rest) => rest, 156 | Err(_) => unreachable!(), 157 | } 158 | } 159 | 160 | pub async fn get_native_client_from_config( 161 | &self, 162 | config: native_app::NativeAppConfig, 163 | ) -> native_app::NativeApp { 164 | match native_app::NativeApp::from_config(config, self.self_reference.clone()) { 165 | Ok(native_app) => native_app, 166 | Err(_) => unreachable!(), 167 | } 168 | } 169 | 170 | pub async fn get_native_client(&self) -> native_app::NativeApp { 171 | match native_app::NativeApp::new(self.self_reference.clone()) { 172 | Ok(native_app) => native_app, 173 | Err(_) => unreachable!(), 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/native_app.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::types; 3 | use serde::{Deserialize, Serialize}; 4 | use std::sync::{Arc, RwLock, Weak}; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct NativeAppConfig { 8 | webhook_id: Option, 9 | cloudhook_url: Option, 10 | remote_ui_url: Option, 11 | secret: Option, 12 | } 13 | 14 | #[derive(Debug)] 15 | pub struct NativeApp { 16 | webhook_id: Option, 17 | cloudhook_url: Option, 18 | remote_ui_url: Option, 19 | secret: Option, 20 | ha_client: Arc>, 21 | } 22 | 23 | impl NativeApp { 24 | pub fn new(ha_client: Weak>) -> Result { 25 | match ha_client.upgrade() { 26 | Some(ha_api) => Ok(Self { 27 | webhook_id: None, 28 | cloudhook_url: None, 29 | remote_ui_url: None, 30 | secret: None, 31 | ha_client: ha_api, 32 | }), 33 | None => Err(errors::Error::HaApi(String::from( 34 | "Weak PTR Upgrade unsececcful", 35 | ))), 36 | } 37 | } 38 | 39 | pub fn from_config( 40 | config: NativeAppConfig, 41 | ha_client: Weak>, 42 | ) -> Result { 43 | match ha_client.upgrade() { 44 | Some(ha_api) => Ok(Self { 45 | webhook_id: config.webhook_id, 46 | cloudhook_url: config.cloudhook_url, 47 | remote_ui_url: config.remote_ui_url, 48 | secret: config.secret, 49 | ha_client: ha_api, 50 | }), 51 | None => Err(errors::Error::HaApi(String::from( 52 | "Weak PTR Upgrade unsececcful", 53 | ))), 54 | } 55 | } 56 | 57 | pub fn set_webhook_info( 58 | &mut self, 59 | webhook_id: String, 60 | cloudhook_url: Option, 61 | remote_ui_url: Option, 62 | ) { 63 | self.webhook_id = Some(webhook_id); 64 | self.cloudhook_url = cloudhook_url; 65 | self.remote_ui_url = remote_ui_url; 66 | } 67 | 68 | pub async fn register_machine( 69 | &mut self, 70 | request: &types::RegisterDeviceRequest, 71 | ) -> Result { 72 | let mut read_lock = self.ha_client.read().unwrap(); //panic 73 | if read_lock.token.need_refresh() { 74 | drop(read_lock); 75 | let mut write_lock = self.ha_client.write().unwrap(); 76 | write_lock.refresh_oauth_token().await?; 77 | read_lock = self.ha_client.read().unwrap(); 78 | } 79 | let endpoint = format!("{}/api/mobile_app/registrations", read_lock.instance_url); 80 | let resp = reqwest::Client::new() 81 | .post(endpoint.as_str()) 82 | .header( 83 | "Authorization", 84 | format!("Bearer {}", read_lock.token.as_string()?), 85 | ) 86 | .json(&request) 87 | .send() 88 | .await?; 89 | drop(read_lock); 90 | let r: types::RegisterDeviceResponse = resp.json().await?; 91 | self.set_webhook_info( 92 | r.webhook_id.clone(), 93 | r.cloud_hook_url.clone(), 94 | r.remote_ui_url.clone(), 95 | ); 96 | Ok(r) 97 | } 98 | 99 | pub async fn register_sensor( 100 | &mut self, 101 | request: &types::SensorRegistrationRequest, 102 | ) -> Result { 103 | let mut read_lock = self.ha_client.read().unwrap(); 104 | if read_lock.token.need_refresh() { 105 | drop(read_lock); 106 | let mut write_lock = self.ha_client.write().unwrap(); 107 | write_lock.refresh_oauth_token().await?; 108 | read_lock = self.ha_client.read().unwrap(); 109 | } 110 | let webhook_id = self 111 | .webhook_id 112 | .as_ref() 113 | .ok_or_else(|| errors::Error::Config("expected webhook_id to exist".to_string()))?; 114 | let endpoint = format!("{}/api/webhook/{}", read_lock.instance_url, webhook_id); 115 | 116 | let response = reqwest::Client::new() 117 | .post(endpoint.as_str()) 118 | .header( 119 | "Authorization", 120 | format!("Bearer {}", read_lock.token.as_string()?), 121 | ) 122 | .json(&request) 123 | .send() 124 | .await?; 125 | drop(read_lock); 126 | let resp_json: types::RegisterSensorResponse = response.json().await?; 127 | Ok(resp_json) 128 | } 129 | 130 | pub async fn update_sensor( 131 | &mut self, 132 | sensor_data: types::SensorUpdateData, 133 | ) -> Result<(), errors::Error> { 134 | let mut read_lock = self.ha_client.read().unwrap(); //panic 135 | if read_lock.token.need_refresh() { 136 | drop(read_lock); 137 | let mut write_lock = self.ha_client.write().unwrap(); 138 | write_lock.refresh_oauth_token().await?; 139 | read_lock = self.ha_client.read().unwrap(); 140 | } 141 | let webhook_id = self 142 | .webhook_id 143 | .as_ref() 144 | .ok_or_else(|| errors::Error::Config("missing webhook id".to_string()))?; 145 | 146 | let endpoint = format!("{}/api/webhook/{}", read_lock.instance_url, webhook_id); 147 | 148 | let request = crate::types::SensorUpdateRequest { 149 | data: sensor_data, 150 | r#type: String::from("update_sensor_states"), 151 | }; 152 | 153 | reqwest::Client::new() 154 | .post(endpoint.as_str()) 155 | .header( 156 | "Authorization", 157 | format!("Bearer {}", read_lock.token.as_string()?), 158 | ) 159 | .json(&request) 160 | .send() 161 | .await?; 162 | 163 | drop(read_lock); 164 | 165 | Ok(()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/rest.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::types; 3 | use serde::{Deserialize, Serialize}; 4 | use std::convert::TryFrom; 5 | use std::sync::{Arc, RwLock, Weak}; 6 | 7 | #[derive(Debug)] 8 | pub struct Rest { 9 | ha_client: Arc>, 10 | } 11 | 12 | impl Rest { 13 | pub async fn check(self) -> Result { 14 | let mut read_lock = self.ha_client.read().unwrap(); 15 | if read_lock.token.need_refresh() { 16 | drop(read_lock); 17 | let mut write_lock = self.ha_client.write().unwrap(); 18 | write_lock.refresh_oauth_token().await?; 19 | read_lock = self.ha_client.read().unwrap(); 20 | } 21 | 22 | let endpoint = format!("{}/api/config", read_lock.instance_url); 23 | let request = reqwest::Client::new() 24 | .get(endpoint.as_str()) 25 | .header( 26 | "Authorization", 27 | format!("Bearer {}", read_lock.token.as_string()?), 28 | ) 29 | .header("content-type", "application/json"); 30 | 31 | drop(read_lock); 32 | let response = request.send().await?; 33 | 34 | #[derive(Serialize, Deserialize, Debug)] 35 | struct Response { 36 | message: String, 37 | } 38 | 39 | let resp_json: Response = response.json().await?; 40 | 41 | Ok(resp_json.message) 42 | 43 | //Err(errors::Error::HaApi(String::from("Not Implemented"))) 44 | } 45 | 46 | pub async fn config(self) -> Result { 47 | let mut read_lock = self.ha_client.read().unwrap(); 48 | if read_lock.token.need_refresh() { 49 | drop(read_lock); 50 | let mut write_lock = self.ha_client.write().unwrap(); 51 | write_lock.refresh_oauth_token().await?; 52 | read_lock = self.ha_client.read().unwrap(); 53 | } 54 | 55 | let endpoint = format!("{}/api/config", read_lock.instance_url); 56 | let request = reqwest::Client::new() 57 | .get(endpoint.as_str()) 58 | .header( 59 | "Authorization", 60 | format!("Bearer {}", read_lock.token.as_string()?), 61 | ) 62 | .header("content-type", "application/json"); 63 | 64 | drop(read_lock); 65 | let response = request.send().await?; 66 | 67 | let resp_json: types::Configuration = response.json().await?; 68 | 69 | Ok(resp_json) 70 | } 71 | 72 | pub async fn discovery_info(self) -> Result { 73 | let mut read_lock = self.ha_client.read().unwrap(); 74 | if read_lock.token.need_refresh() { 75 | drop(read_lock); 76 | let mut write_lock = self.ha_client.write().unwrap(); 77 | write_lock.refresh_oauth_token().await?; 78 | read_lock = self.ha_client.read().unwrap(); 79 | } 80 | 81 | let endpoint = format!("{}/api/discovery_info", read_lock.instance_url); 82 | let request = reqwest::Client::new() 83 | .get(endpoint.as_str()) 84 | .header( 85 | "Authorization", 86 | format!("Bearer {}", read_lock.token.as_string()?), 87 | ) 88 | .header("content-type", "application/json"); 89 | 90 | drop(read_lock); 91 | let response = request.send().await?; 92 | 93 | let resp_json: types::DiscoveryInfo = response.json().await?; 94 | 95 | Ok(resp_json) 96 | } 97 | 98 | pub async fn events(self) -> Result, errors::Error> { 99 | let mut read_lock = self.ha_client.read().unwrap(); 100 | if read_lock.token.need_refresh() { 101 | drop(read_lock); 102 | let mut write_lock = self.ha_client.write().unwrap(); 103 | write_lock.refresh_oauth_token().await?; 104 | read_lock = self.ha_client.read().unwrap(); 105 | } 106 | 107 | let endpoint = format!("{}/api/events", read_lock.instance_url); 108 | let request = reqwest::Client::new() 109 | .get(endpoint.as_str()) 110 | .header( 111 | "Authorization", 112 | format!("Bearer {}", read_lock.token.as_string()?), 113 | ) 114 | .header("content-type", "application/json"); 115 | 116 | drop(read_lock); 117 | let response = request.send().await?; 118 | 119 | let resp_json: Vec = response.json().await?; 120 | 121 | Ok(resp_json) 122 | } 123 | 124 | pub async fn services(self) -> Result, errors::Error> { 125 | let mut read_lock = self.ha_client.read().unwrap(); 126 | if read_lock.token.need_refresh() { 127 | drop(read_lock); 128 | let mut write_lock = self.ha_client.write().unwrap(); 129 | write_lock.refresh_oauth_token().await?; 130 | read_lock = self.ha_client.read().unwrap(); 131 | } 132 | 133 | let endpoint = format!("{}/api/services", read_lock.instance_url); 134 | let request = reqwest::Client::new() 135 | .get(endpoint.as_str()) 136 | .header( 137 | "Authorization", 138 | format!("Bearer {}", read_lock.token.as_string()?), 139 | ) 140 | .header("content-type", "application/json"); 141 | 142 | drop(read_lock); 143 | let response = request.send().await?; 144 | 145 | let resp_json: Vec = response.json().await?; 146 | 147 | Ok(resp_json) 148 | } 149 | 150 | pub async fn history_period( 151 | self, 152 | timestamp: Option>, 153 | filter_entity_id: Option, 154 | end_time: Option>, 155 | significant_changes_only: Option, 156 | ) -> Result, errors::Error> { 157 | let mut read_lock = self.ha_client.read().unwrap(); 158 | if read_lock.token.need_refresh() { 159 | drop(read_lock); 160 | let mut write_lock = self.ha_client.write().unwrap(); 161 | write_lock.refresh_oauth_token().await?; 162 | read_lock = self.ha_client.read().unwrap(); 163 | } 164 | 165 | let mut endpoint = format!("{}/api/history/period", read_lock.instance_url); 166 | 167 | if let Some(timestamp) = timestamp { 168 | let formatted_timestamp = timestamp.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); 169 | endpoint = endpoint + &formatted_timestamp; 170 | } 171 | 172 | let mut request = reqwest::Client::new() 173 | .get(endpoint.as_str()) 174 | .header( 175 | "Authorization", 176 | format!("Bearer {}", read_lock.token.as_string()?), 177 | ) 178 | .header("content-type", "application/json"); 179 | 180 | drop(read_lock); 181 | 182 | if let Some(filter_entity_id) = filter_entity_id { 183 | request = request.query(&[("filter_entity_id", filter_entity_id)]); 184 | } 185 | 186 | if let Some(end_time) = end_time { 187 | let formatted_timestamp = end_time.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); 188 | request = request.query(&[("end_time", formatted_timestamp)]); 189 | } 190 | 191 | if significant_changes_only.is_some() { 192 | request = request.query(&[("significant_changes_only")]); 193 | } 194 | 195 | let response = request.send().await?; 196 | 197 | let resp_json: Vec = response.json().await?; 198 | 199 | Ok(resp_json) 200 | } 201 | 202 | pub async fn history_period_minimal( 203 | self, 204 | timestamp: Option>, 205 | filter_entity_id: Option, 206 | end_time: Option>, 207 | significant_changes_only: Option, 208 | ) -> Result, errors::Error> { 209 | let mut read_lock = self.ha_client.read().unwrap(); 210 | if read_lock.token.need_refresh() { 211 | drop(read_lock); 212 | let mut write_lock = self.ha_client.write().unwrap(); 213 | write_lock.refresh_oauth_token().await?; 214 | read_lock = self.ha_client.read().unwrap(); 215 | } 216 | 217 | let mut endpoint = format!("{}/api/history/period", read_lock.instance_url); 218 | 219 | if let Some(timestamp) = timestamp { 220 | let formatted_timestamp = timestamp.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); 221 | endpoint = endpoint + &formatted_timestamp; 222 | } 223 | 224 | let mut request = reqwest::Client::new() 225 | .get(endpoint.as_str()) 226 | .header( 227 | "Authorization", 228 | format!("Bearer {}", read_lock.token.as_string()?), 229 | ) 230 | .header("content-type", "application/json"); 231 | 232 | drop(read_lock); 233 | 234 | if let Some(filter_entity_id) = filter_entity_id { 235 | request = request.query(&[("filter_entity_id", filter_entity_id)]); 236 | } 237 | 238 | if let Some(end_time) = end_time { 239 | let formatted_timestamp = end_time.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); 240 | request = request.query(&[("end_time", formatted_timestamp)]); 241 | } 242 | 243 | if significant_changes_only.is_some() { 244 | request = request.query(&[("significant_changes_only")]); 245 | } 246 | 247 | request = request.query(&[("minimal_response")]); 248 | 249 | let response = request.send().await?; 250 | 251 | let resp_json: Vec = response.json().await?; 252 | 253 | Ok(resp_json) 254 | } 255 | 256 | pub async fn logbook( 257 | self, 258 | timestamp: Option>, 259 | entity: String, 260 | end_time: Option>, 261 | ) -> Result, errors::Error> { 262 | let mut read_lock = self.ha_client.read().unwrap(); 263 | if read_lock.token.need_refresh() { 264 | drop(read_lock); 265 | let mut write_lock = self.ha_client.write().unwrap(); 266 | write_lock.refresh_oauth_token().await?; 267 | read_lock = self.ha_client.read().unwrap(); 268 | } 269 | 270 | let mut endpoint = format!("{}/api/logbook", read_lock.instance_url); 271 | 272 | if let Some(timestamp) = timestamp { 273 | let formatted_timestamp = timestamp.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); 274 | endpoint = endpoint + &formatted_timestamp; 275 | } 276 | 277 | let mut request = reqwest::Client::new() 278 | .get(endpoint.as_str()) 279 | .header( 280 | "Authorization", 281 | format!("Bearer {}", read_lock.token.as_string()?), 282 | ) 283 | .header("content-type", "application/json"); 284 | 285 | drop(read_lock); 286 | 287 | request = request.query(&[("entity", entity)]); 288 | 289 | if let Some(end_time) = end_time { 290 | let formatted_timestamp = end_time.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); 291 | request = request.query(&[("end_time", formatted_timestamp)]); 292 | } 293 | 294 | let response = request.send().await?; 295 | 296 | let resp_json: Vec = response.json().await?; 297 | 298 | Ok(resp_json) 299 | } 300 | 301 | pub async fn states(self) -> Result, errors::Error> { 302 | let mut read_lock = self.ha_client.read().unwrap(); 303 | if read_lock.token.need_refresh() { 304 | drop(read_lock); 305 | let mut write_lock = self.ha_client.write().unwrap(); 306 | write_lock.refresh_oauth_token().await?; 307 | read_lock = self.ha_client.read().unwrap(); 308 | } 309 | 310 | let endpoint = format!("{}/api/states", read_lock.instance_url); 311 | let request = reqwest::Client::new() 312 | .get(endpoint.as_str()) 313 | .header( 314 | "Authorization", 315 | format!("Bearer {}", read_lock.token.as_string()?), 316 | ) 317 | .header("content-type", "application/json"); 318 | 319 | drop(read_lock); 320 | let response = request.send().await?; 321 | 322 | let resp_json: Vec = response.json().await?; 323 | 324 | Ok(resp_json) 325 | } 326 | 327 | pub async fn state_of( 328 | self, 329 | entity_id: String, 330 | ) -> Result, errors::Error> { 331 | let mut read_lock = self.ha_client.read().unwrap(); 332 | if read_lock.token.need_refresh() { 333 | drop(read_lock); 334 | let mut write_lock = self.ha_client.write().unwrap(); 335 | write_lock.refresh_oauth_token().await?; 336 | read_lock = self.ha_client.read().unwrap(); 337 | } 338 | 339 | let endpoint = format!("{}/api/states/{}", read_lock.instance_url, entity_id); 340 | let request = reqwest::Client::new() 341 | .get(endpoint.as_str()) 342 | .header( 343 | "Authorization", 344 | format!("Bearer {}", read_lock.token.as_string()?), 345 | ) 346 | .header("content-type", "application/json"); 347 | 348 | drop(read_lock); 349 | let response = request.send().await?; 350 | let resp_json: Vec = response.json().await?; 351 | 352 | Ok(resp_json) 353 | } 354 | 355 | pub async fn error_log(self) -> Result { 356 | let mut read_lock = self.ha_client.read().unwrap(); 357 | if read_lock.token.need_refresh() { 358 | drop(read_lock); 359 | let mut write_lock = self.ha_client.write().unwrap(); 360 | write_lock.refresh_oauth_token().await?; 361 | read_lock = self.ha_client.read().unwrap(); 362 | } 363 | 364 | let endpoint = format!("{}/api/error_log", read_lock.instance_url); 365 | let request = reqwest::Client::new() 366 | .get(endpoint.as_str()) 367 | .header( 368 | "Authorization", 369 | format!("Bearer {}", read_lock.token.as_string()?), 370 | ) 371 | .header("content-type", "application/json"); 372 | 373 | drop(read_lock); 374 | let response = request.send().await?; 375 | 376 | let resp: String = response.text().await?; 377 | 378 | Ok(resp) 379 | } 380 | 381 | pub async fn camera_proxy(self, camera_entity_id: String) -> Result<(), errors::Error> { 382 | let mut read_lock = self.ha_client.read().unwrap(); 383 | if read_lock.token.need_refresh() { 384 | drop(read_lock); 385 | let mut write_lock = self.ha_client.write().unwrap(); 386 | write_lock.refresh_oauth_token().await?; 387 | read_lock = self.ha_client.read().unwrap(); 388 | } 389 | 390 | let endpoint = format!( 391 | "{}/api/camera_proxy/{}", 392 | read_lock.instance_url, camera_entity_id 393 | ); 394 | let request = reqwest::Client::new() 395 | .get(endpoint.as_str()) 396 | .header( 397 | "Authorization", 398 | format!("Bearer {}", read_lock.token.as_string()?), 399 | ) 400 | .header("content-type", "application/json"); 401 | 402 | drop(read_lock); 403 | let _response = request.send().await?; 404 | 405 | Ok(()) 406 | } 407 | 408 | pub async fn state_change( 409 | self, 410 | entity_id: String, 411 | state_data: Option, 412 | ) -> Result { 413 | let mut read_lock = self.ha_client.read().unwrap(); 414 | if read_lock.token.need_refresh() { 415 | drop(read_lock); 416 | let mut write_lock = self.ha_client.write().unwrap(); 417 | write_lock.refresh_oauth_token().await?; 418 | read_lock = self.ha_client.read().unwrap(); 419 | } 420 | 421 | let endpoint = format!("{}/api/states/{}", read_lock.instance_url, entity_id); 422 | let mut request = reqwest::Client::new() 423 | .post(endpoint.as_str()) 424 | .header( 425 | "Authorization", 426 | format!("Bearer {}", read_lock.token.as_string()?), 427 | ) 428 | .header("content-type", "application/json"); 429 | 430 | drop(read_lock); 431 | 432 | if let Some(data) = state_data { 433 | request = request.json(&data); 434 | } 435 | 436 | let response = request.send().await?; 437 | 438 | let resp_json: types::StateObject = response.json().await?; 439 | 440 | Ok(resp_json) 441 | } 442 | 443 | pub async fn event_fire( 444 | self, 445 | event_type: String, 446 | event_data: Option, 447 | ) -> Result { 448 | let mut read_lock = self.ha_client.read().unwrap(); 449 | if read_lock.token.need_refresh() { 450 | drop(read_lock); 451 | let mut write_lock = self.ha_client.write().unwrap(); 452 | write_lock.refresh_oauth_token().await?; 453 | read_lock = self.ha_client.read().unwrap(); 454 | } 455 | 456 | let endpoint = format!("{}/api/events/{}", read_lock.instance_url, event_type); 457 | let mut request = reqwest::Client::new() 458 | .post(endpoint.as_str()) 459 | .header( 460 | "Authorization", 461 | format!("Bearer {}", read_lock.token.as_string()?), 462 | ) 463 | .header("content-type", "application/json"); 464 | 465 | drop(read_lock); 466 | 467 | if let Some(data) = event_data { 468 | request = request.json(&data); 469 | } 470 | 471 | let response = request.send().await?; 472 | 473 | #[derive(Serialize, Deserialize, Debug)] 474 | struct Response { 475 | message: String, 476 | } 477 | 478 | let resp_json: Response = response.json().await?; 479 | 480 | Ok(resp_json.message) 481 | } 482 | 483 | pub async fn service_call( 484 | self, 485 | domain: String, 486 | service: String, 487 | service_data: Option, 488 | ) -> Result, errors::Error> { 489 | let mut read_lock = self.ha_client.read().unwrap(); 490 | if read_lock.token.need_refresh() { 491 | drop(read_lock); 492 | let mut write_lock = self.ha_client.write().unwrap(); 493 | write_lock.refresh_oauth_token().await?; 494 | read_lock = self.ha_client.read().unwrap(); 495 | } 496 | 497 | let endpoint = format!( 498 | "{}/api/services/{}/{}", 499 | read_lock.instance_url, domain, service 500 | ); 501 | let mut request = reqwest::Client::new() 502 | .post(endpoint.as_str()) 503 | .header( 504 | "Authorization", 505 | format!("Bearer {}", read_lock.token.as_string()?), 506 | ) 507 | .header("content-type", "application/json"); 508 | 509 | drop(read_lock); 510 | 511 | if let Some(data) = service_data { 512 | request = request.json(&data); 513 | } 514 | 515 | let response = request.send().await?; 516 | 517 | let resp_json: Vec = response.json().await?; 518 | 519 | Ok(resp_json) 520 | } 521 | 522 | pub async fn template_render(self, template: String) -> Result { 523 | let mut read_lock = self.ha_client.read().unwrap(); 524 | if read_lock.token.need_refresh() { 525 | drop(read_lock); 526 | let mut write_lock = self.ha_client.write().unwrap(); 527 | write_lock.refresh_oauth_token().await?; 528 | read_lock = self.ha_client.read().unwrap(); 529 | } 530 | 531 | let endpoint = format!("{}/api/template", read_lock.instance_url); 532 | 533 | #[derive(Serialize, Deserialize, Debug)] 534 | struct Template { 535 | template: String, 536 | } 537 | 538 | let template_struct = Template { template }; 539 | let request = reqwest::Client::new() 540 | .post(endpoint.as_str()) 541 | .header( 542 | "Authorization", 543 | format!("Bearer {}", read_lock.token.as_string()?), 544 | ) 545 | .header("content-type", "application/json"); 546 | 547 | drop(read_lock); 548 | 549 | let response = request.json(&template_struct).send().await?; 550 | 551 | let resp: String = response.text().await?; 552 | 553 | Ok(resp) 554 | } 555 | 556 | pub async fn check_config(self) -> Result { 557 | let mut read_lock = self.ha_client.read().unwrap(); 558 | if read_lock.token.need_refresh() { 559 | drop(read_lock); 560 | let mut write_lock = self.ha_client.write().unwrap(); 561 | write_lock.refresh_oauth_token().await?; 562 | read_lock = self.ha_client.read().unwrap(); 563 | } 564 | 565 | let endpoint = format!("{}/api/config/core/check_config", read_lock.instance_url); 566 | let response = reqwest::Client::new() 567 | .post(endpoint.as_str()) 568 | .header( 569 | "Authorization", 570 | format!("Bearer {}", read_lock.token.as_string()?), 571 | ) 572 | .header("content-type", "application/json") 573 | .send() 574 | .await?; 575 | 576 | drop(read_lock); 577 | 578 | let resp_json: types::CheckConfig = response.json().await?; 579 | 580 | Ok(resp_json) 581 | } 582 | } 583 | 584 | impl TryFrom>> for Rest { 585 | type Error = errors::Error; 586 | 587 | fn try_from(weak: Weak>) -> Result { 588 | match weak.upgrade() { 589 | Some(ptr) => Ok(Self { ha_client: ptr }), 590 | None => Err(errors::Error::HaApi(String::from( 591 | "Can't create Rest Client weak ptr returned none", 592 | ))), 593 | } 594 | } 595 | } 596 | 597 | impl From>> for Rest { 598 | fn from(ptr: Arc>) -> Self { 599 | Self { ha_client: ptr } 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct HaEntityAttribute { 5 | pub friendly_name: Option, 6 | } 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | pub struct HaEntityState { 10 | pub attributes: HaEntityAttribute, 11 | } 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct RegisterDeviceRequest { 15 | pub device_id: String, 16 | pub app_id: String, 17 | pub app_name: String, 18 | pub app_version: String, 19 | pub device_name: String, 20 | pub manufacturer: String, 21 | pub model: String, 22 | pub os_name: String, 23 | pub os_version: String, 24 | pub supports_encryption: bool, 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Debug)] 28 | pub struct RegisterDeviceResponse { 29 | pub cloud_hook_url: Option, 30 | pub remote_ui_url: Option, 31 | pub secret: Option, 32 | pub webhook_id: String, 33 | } 34 | 35 | #[derive(Serialize, Deserialize, Debug)] 36 | pub struct GetAccessTokenRequest { 37 | pub grant_type: String, 38 | pub code: String, 39 | pub client_id: String, 40 | } 41 | #[derive(Serialize, Deserialize, Debug)] 42 | pub struct GetAccessTokenResponse { 43 | pub access_token: String, 44 | pub expires_in: u32, 45 | pub refresh_token: String, 46 | pub token_type: String, 47 | } 48 | 49 | #[derive(Serialize, Deserialize, Debug)] 50 | pub struct RefreshAccessTokenResponse { 51 | pub access_token: String, 52 | pub expires_in: u32, 53 | pub token_type: String, 54 | } 55 | 56 | #[derive(Serialize, Deserialize, Debug)] 57 | pub struct GetAccessTokenError { 58 | pub error: String, 59 | pub error_description: String, 60 | } 61 | 62 | #[derive(Serialize, Deserialize, Debug)] 63 | pub struct SensorRegistrationRequest { 64 | pub r#type: String, 65 | pub data: SensorRegistrationData, 66 | } 67 | 68 | #[derive(Serialize, Deserialize, Debug)] 69 | pub struct SensorRegistrationData { 70 | pub device_class: Option, 71 | pub icon: String, 72 | pub name: String, 73 | pub state: String, 74 | pub r#type: String, 75 | pub unique_id: String, 76 | pub unit_of_measurement: String, 77 | pub attributes: std::collections::HashMap, 78 | } 79 | 80 | #[derive(Serialize, Deserialize, Debug)] 81 | pub struct RegisterSensorResponse { 82 | pub success: bool, 83 | } 84 | 85 | #[derive(Serialize, Deserialize, Debug)] 86 | pub struct SensorUpdateRequest { 87 | pub r#type: String, 88 | pub data: SensorUpdateData, 89 | } 90 | 91 | #[derive(Serialize, Deserialize, Debug)] 92 | pub struct SensorUpdateData { 93 | pub icon: String, 94 | pub state: String, 95 | pub r#type: String, 96 | pub unique_id: String, 97 | pub attributes: std::collections::HashMap, 98 | } 99 | 100 | #[derive(Serialize, Deserialize, Debug)] 101 | pub struct Configuration { 102 | pub components: Vec, 103 | pub config_dir: String, 104 | pub elevation: f64, 105 | pub latitude: f64, 106 | pub location_name: String, 107 | pub longitude: f64, 108 | pub time_zone: String, 109 | pub unit_system: UnitSystem, 110 | pub version: String, 111 | pub whitelist_external_dirs: Vec, 112 | } 113 | 114 | #[derive(Serialize, Deserialize, Debug)] 115 | pub struct UnitSystem { 116 | pub length: String, 117 | pub mass: String, 118 | pub temperature: String, 119 | pub volume: String, 120 | } 121 | 122 | #[derive(Serialize, Deserialize, Debug)] 123 | pub struct DiscoveryInfo { 124 | pub base_url: String, 125 | pub location_name: String, 126 | pub requires_api_password: bool, 127 | pub version: String, 128 | } 129 | 130 | #[derive(Serialize, Deserialize, Debug)] 131 | pub struct EventObject { 132 | pub event: String, 133 | pub listener_count: u32, 134 | } 135 | 136 | #[derive(Serialize, Deserialize, Debug)] 137 | pub struct ServiceObject { 138 | pub domain: String, 139 | pub services: Vec, 140 | } 141 | 142 | #[derive(Serialize, Deserialize, Debug)] 143 | pub struct StateObject { 144 | pub attributes: std::collections::HashMap, 145 | pub entity_id: String, 146 | pub last_changed: String, 147 | pub last_updated: Option, 148 | pub state: String, 149 | } 150 | 151 | #[derive(Serialize, Deserialize, Debug)] 152 | pub struct LogbookEntry { 153 | pub context_user_id: String, 154 | pub domain: String, 155 | pub entity_id: String, 156 | pub message: String, 157 | pub name: String, 158 | pub when: String, 159 | } 160 | 161 | #[derive(Serialize, Deserialize, Debug)] 162 | pub struct CheckConfig { 163 | pub errors: String, 164 | pub result: String, 165 | } 166 | --------------------------------------------------------------------------------