homeassistant_rs/
lib.rs

1//! Implements the Homeassistant API for use in rust
2//!
3//! A simple lib, that queries different endpoints and returns the data in a usable format.
4//!
5//! The first 2 arguments of each function are always: `HA_URL`, `API_Token`.
6//!
7//! These arguments do not have to be filled with actual data, they can be `None`, but in this case you will need to use environment variables.
8//!
9//! Under the hood we use dotenvy.
10//!
11//! Example env:
12//! ```text
13//! HA_URL="http://localhost:8123"
14//! HA_TOKEN="api_token_from_hass"
15//! ```
16//!
17//! - Easily get HA's config:
18//! ```
19//! # use tokio::runtime::Runtime;
20//! # let rt = Runtime::new().unwrap();
21//! # rt.block_on(async {
22//! use homeassistant_rs::{self, hass};
23//! let config = hass().config(None, None).await.unwrap();
24//!
25//! println!("{}", config.version);
26//! # });
27//! ```
28//!
29//! You can check all available endpoints here: [`HomeAssistant`]
30//!
31//! - More Examples:
32//!
33//!
34//! ```
35//! # use tokio::runtime::Runtime;
36//! # let rt = Runtime::new().unwrap();
37//! # rt.block_on(async {
38//! use homeassistant_rs::hass;
39//! 
40//! hass().config(None, None).await.unwrap();
41//! hass().events(None, None).await.unwrap();
42//! hass().services(None, None).await.unwrap();
43//! hass()
44//!     .history(
45//!         None,
46//!         None,
47//!         Some("light.bedroom_light_shelly"),
48//!         /// minimal_response
49//!         true,
50//!         /// no_attributes
51//!         true,
52//!         /// significant_changes_only
53//!         true,
54//!     )
55//!     .await.unwrap();
56//! hass().logbook(None, None, Some("light.bedroom_light_shelly")).await.unwrap();
57//! hass().states(None, None, Some("light.bedroom_light_shelly")).await.unwrap();
58//! hass().states(None, None, None).await.unwrap();
59//! hass().error_log(None, None).await.unwrap();
60//!  # });
61//! ```
62
63#[cfg(test)]
64mod tests;
65pub use ::bytes;
66pub use ::lazy_static;
67pub use ::reqwest;
68pub use ::serde;
69pub use ::serde_json;
70use serde_json::json;
71
72pub mod structs;
73
74// ### BEGIN INTERNAL USE ONLY ###
75
76lazy_static::lazy_static! {
77    pub static ref CLIENT: reqwest::Client = reqwest::Client::new();
78
79    static ref GLOBAL_VARS: GlobalVars = GlobalVars::new();
80}
81
82struct GlobalVars {
83    url: Option<String>,
84    token: Option<String>,
85}
86
87impl GlobalVars {
88    fn new() -> Self {
89        Self {
90            url: dotenvy::var("HA_URL").ok(),
91            token: dotenvy::var("HA_TOKEN").ok(),
92        }
93    }
94}
95
96fn globalvars() -> &'static GlobalVars {
97    GlobalVars::new();
98    &GLOBAL_VARS
99}
100
101struct Validate;
102
103impl Validate {
104    fn arg(&self, str: Option<String>) -> anyhow::Result<String, anyhow::Error> {
105        if let Some(str) = str {
106            Ok(str)
107        } else {
108            Err(anyhow::Error::msg("Seems empty"))
109        }
110    }
111}
112
113fn validate() -> Validate {
114    Validate
115}
116
117async fn request(url: String, token: String, path: &str) -> anyhow::Result<reqwest::Response> {
118    Ok(CLIENT
119        .get(url.to_owned() + path)
120        .bearer_auth(token)
121        .send()
122        .await?)
123}
124
125async fn post<T: serde::Serialize>(
126    url: String,
127    token: String,
128    path: &str,
129    json: T,
130) -> anyhow::Result<reqwest::Response> {
131    if !serde_json::to_string(&json)?.is_empty() {
132        Ok(CLIENT
133            .post(url.to_owned() + path)
134            .bearer_auth(token)
135            .json(&json)
136            .send()
137            .await?)
138    } else {
139        Ok(CLIENT
140            .post(url.to_owned() + path)
141            .bearer_auth(token)
142            .send()
143            .await?)
144    }
145}
146
147// ### END INTERNAL USE ONLY ###
148
149pub struct HomeAssistant;
150
151impl HomeAssistant {
152    pub fn request(&self) -> &'static HomeAssistantPost {
153        &HomeAssistantPost
154    }
155
156    /// queries `/api/config` and returns [`ConfigResponse`](structs::ConfigResponse) struct
157    pub async fn config(
158        &self,
159        ha_url: Option<String>,
160        ha_token: Option<String>,
161    ) -> anyhow::Result<structs::ConfigResponse> {
162        let vars = globalvars();
163        let url = validate().arg(ha_url).or_else(|_| {
164            vars.url
165                .clone()
166                .ok_or(anyhow::Error::msg("HA_URL is required"))
167        })?;
168        let token = validate().arg(ha_token).or_else(|_| {
169            vars.token
170                .clone()
171                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
172        })?;
173
174        let client = request(url, token, "/api/config").await?;
175        if !client.status().is_success() {
176            Err(anyhow::Error::msg(client.status()))
177        } else {
178            Ok(client.json::<structs::ConfigResponse>().await?)
179        }
180    }
181
182    /// queries `/api/events` and returns a Vec containing [`EventResponse`](structs::EventResponse) struct    
183    pub async fn events(
184        &self,
185        ha_url: Option<String>,
186        ha_token: Option<String>,
187    ) -> anyhow::Result<Vec<structs::EventResponse>> {
188        let vars = globalvars();
189        let url = validate().arg(ha_url).or_else(|_| {
190            vars.url
191                .clone()
192                .ok_or(anyhow::Error::msg("HA_URL is required"))
193        })?;
194        let token = validate().arg(ha_token).or_else(|_| {
195            vars.token
196                .clone()
197                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
198        })?;
199
200        let client = request(url, token, "/api/events").await?;
201
202        if !client.status().is_success() {
203            Err(anyhow::Error::msg(client.status()))
204        } else {
205            Ok(client.json::<Vec<structs::EventResponse>>().await?)
206        }
207    }
208
209    /// queries `/api/services` and returns a Vec containing [`ServicesResponse`](structs::ServicesResponse) (subject to possibly change in the future)
210    pub async fn services(
211        &self,
212        ha_url: Option<String>,
213        ha_token: Option<String>,
214    ) -> anyhow::Result<Vec<structs::ServicesResponse>> {
215        let vars = globalvars();
216        let url = validate().arg(ha_url).or_else(|_| {
217            vars.url
218                .clone()
219                .ok_or(anyhow::Error::msg("HA_URL is required"))
220        })?;
221        let token = validate().arg(ha_token).or_else(|_| {
222            vars.token
223                .clone()
224                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
225        })?;
226
227        let client = request(url, token, "/api/services").await?.json::<Vec<structs::ServicesResponse>>().await?;
228
229        Ok(client)
230    }
231
232    /// queries `/api/history/period/<optionalargs>` and returns a Vec containing [`HistoryResponse`](structs::HistoryResponse) struct
233    pub async fn history(
234        &self,
235        ha_url: Option<String>,
236        ha_token: Option<String>,
237        ha_entity_id: Option<&str>,
238        minimal_response: bool,
239        no_attributes: bool,
240        significant_changes_only: bool,
241    ) -> anyhow::Result<Vec<structs::HistoryResponse>> {
242        let vars = globalvars();
243        let url = validate().arg(ha_url).or_else(|_| {
244            vars.url
245                .clone()
246                .ok_or(anyhow::Error::msg("HA_URL is required"))
247        })?;
248        let token = validate().arg(ha_token).or_else(|_| {
249            vars.token
250                .clone()
251                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
252        })?;
253
254        let path = format!(
255            "?filter_entity_id={0}{1}{2}{3}",
256            ha_entity_id.unwrap_or(""),
257            if minimal_response {
258                "&minimal_response"
259            } else {
260                ""
261            },
262            if no_attributes { "&no_attributes" } else { "" },
263            if significant_changes_only {
264                "&significant_changes_only"
265            } else {
266                ""
267            }
268        );
269
270        let client = request(url, token, &format!("/api/history/period{path}")).await?;
271
272        if !client.status().is_success() {
273            Err(anyhow::Error::msg(client.status()))
274        } else {
275            Ok(client
276                .json::<Vec<Vec<structs::HistoryResponse>>>()
277                .await?
278                .into_iter()
279                .flatten()
280                .collect())
281        }
282    }
283
284    /// queries `/api/logbook` and returns a Vec containing [`LogBook`](structs::LogBook) struct
285    pub async fn logbook(
286        &self,
287        ha_url: Option<String>,
288        ha_token: Option<String>,
289        ha_entity_id: Option<&str>,
290    ) -> anyhow::Result<Vec<structs::LogBook>> {
291        let vars = globalvars();
292        let url = validate().arg(ha_url).or_else(|_| {
293            vars.url
294                .clone()
295                .ok_or(anyhow::Error::msg("HA_URL is required"))
296        })?;
297        let token = validate().arg(ha_token).or_else(|_| {
298            vars.token
299                .clone()
300                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
301        })?;
302
303        let client = request(
304            url,
305            token,
306            &format!(
307                "/api/logbook{0}",
308                ("?".to_owned() + ha_entity_id.unwrap_or(""))
309            ),
310        )
311        .await?;
312        if !client.status().is_success() {
313            Err(anyhow::Error::msg(client.status()))
314        } else {
315            Ok(client.json::<Vec<structs::LogBook>>().await?)
316        }
317    }
318
319    /// queries `/api/states/<optional_entity_id>` and returns a Vec containing [`StatesResponse`](structs::StatesResponse) struct
320    pub async fn states(
321        &self,
322        ha_url: Option<String>,
323        ha_token: Option<String>,
324        ha_entity_id: Option<&str>,
325    ) -> anyhow::Result<Vec<structs::StatesResponse>> {
326        let vars = globalvars();
327        let url = validate().arg(ha_url).or_else(|_| {
328            vars.url
329                .clone()
330                .ok_or(anyhow::Error::msg("HA_URL is required"))
331        })?;
332        let token = validate().arg(ha_token).or_else(|_| {
333            vars.token
334                .clone()
335                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
336        })?;
337
338        let entity_id = ha_entity_id.unwrap_or_default();
339
340        let client = if entity_id.is_empty() {
341            request(url, token, "/api/states")
342                .await?
343                .json::<Vec<structs::StatesResponse>>()
344                .await?
345        } else {
346            vec![
347                request(url, token, &format!("/api/states/{entity_id}"))
348                    .await?
349                    .json::<structs::StatesResponse>()
350                    .await?,
351            ]
352        };
353
354        Ok(client)
355    }
356
357    /// queries `/api/error_log` and returns a [`String`]
358    pub async fn error_log(
359        &self,
360        ha_url: Option<String>,
361        ha_token: Option<String>,
362    ) -> anyhow::Result<String> {
363        let vars = globalvars();
364        let url = validate().arg(ha_url).or_else(|_| {
365            vars.url
366                .clone()
367                .ok_or(anyhow::Error::msg("HA_URL is required"))
368        })?;
369        let token = validate().arg(ha_token).or_else(|_| {
370            vars.token
371                .clone()
372                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
373        })?;
374
375        let client = request(url, token, "/api/states").await?.text().await?;
376
377        Ok(client)
378    }
379
380    /// queries `/api/camera_proxy/<camera_entity_id>?time=<timestamp>` and returns [`Bytes`](bytes::Bytes)
381    ///
382    /// input parameter `time` as `unix_time` in seconds ([`u64`])
383    ///
384    /// <sub>WARNING: Further testing is required for this function, as i (Blexyel) am not able to test it myself</sub>
385    pub async fn camera_proxy(
386        &self,
387        ha_url: Option<String>,
388        ha_token: Option<String>,
389        ha_entity_id: &str,
390        time: u64,
391    ) -> anyhow::Result<bytes::Bytes> {
392        let vars = globalvars();
393        let url = validate().arg(ha_url).or_else(|_| {
394            vars.url
395                .clone()
396                .ok_or(anyhow::Error::msg("HA_URL is required"))
397        })?;
398        let token = validate().arg(ha_token).or_else(|_| {
399            vars.token
400                .clone()
401                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
402        })?;
403
404        let client = request(
405            url,
406            token,
407            &format!("/api/camera_proxy/{ha_entity_id}?time={time}"),
408        )
409        .await?
410        .bytes()
411        .await?;
412
413        Ok(client)
414    }
415
416    /// queries `/api/calendars/<calendar entity_id>?start=<timestamp>&end=<timestamp>` and returns a Vec containing `[CalendarResponse`](structs::CalendarResponse)
417    #[allow(unreachable_code, unused_variables)]
418    pub async fn calendars(
419        &self,
420        ha_url: Option<String>,
421        ha_token: Option<String>,
422    ) -> anyhow::Result<Vec<structs::CalendarResponse>> {
423        unimplemented!(
424            "I (Blexyel) am unable to implement this function, as (apparently) my HASS instance does not have calendars. Feel free to make a PR to implement this feature"
425        );
426        {
427            let vars = globalvars();
428            let url = validate().arg(ha_url).or_else(|_| {
429                vars.url
430                    .clone()
431                    .ok_or(anyhow::Error::msg("HA_URL is required"))
432            })?;
433            let token = validate().arg(ha_token).or_else(|_| {
434                vars.token
435                    .clone()
436                    .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
437            })?;
438
439            let client = request(url, token, "/api/calendars").await?.bytes().await?;
440
441            Ok(vec![structs::CalendarResponse {
442                entity_id: todo!(),
443                name: todo!(),
444            }])
445        }
446    }
447}
448
449pub struct HomeAssistantPost;
450
451impl HomeAssistantPost {
452    /// posts to `/api/states/<entity_id>` to update/create a state and returns [`StatesResponse`](structs::StatesResponse)
453    pub async fn state(
454        &self,
455        ha_url: Option<String>,
456        ha_token: Option<String>,
457        ha_entity_id: &str,
458        request: structs::StatesRequest,
459    ) -> anyhow::Result<structs::StatesResponse> {
460        let vars = globalvars();
461        let url = validate().arg(ha_url).or_else(|_| {
462            vars.url
463                .clone()
464                .ok_or(anyhow::Error::msg("HA_URL is required"))
465        })?;
466        let token = validate().arg(ha_token).or_else(|_| {
467            vars.token
468                .clone()
469                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
470        })?;
471
472        let client = post(url, token, &format!("/api/states/{ha_entity_id}"), request).await?;
473        if !client.status().is_success() {
474            Err(anyhow::Error::msg(client.status()))
475        } else {
476            Ok(client.json::<structs::StatesResponse>().await?)
477        }
478    }
479    // I have been programming for ~7 Hours straight, I'm tired
480
481    /// posts to `/api/events/<event_type>` to update/create a state and returns [`StatesResponse`](structs::StatesResponse)
482    ///
483    /// request param does not need to have data, it can be empty, e.g.:
484    /// ```ignore
485    /// json!({})
486    /// ```
487    pub async fn events(
488        &self,
489        ha_url: Option<String>,
490        ha_token: Option<String>,
491        ha_event_type: &str,
492        request: serde_json::Value,
493    ) -> anyhow::Result<structs::SimpleResponse> {
494        let vars = globalvars();
495        let url = validate().arg(ha_url).or_else(|_| {
496            vars.url
497                .clone()
498                .ok_or(anyhow::Error::msg("HA_URL is required"))
499        })?;
500        let token = validate().arg(ha_token).or_else(|_| {
501            vars.token
502                .clone()
503                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
504        })?;
505
506        let client = post(url, token, &format!("/api/events/{ha_event_type}"), request).await?;
507
508        if !client.status().is_success() {
509            Err(anyhow::Error::msg(client.status()))
510        } else {
511            Ok(client.json::<structs::SimpleResponse>().await?)
512        }
513    }
514
515    /// posts to `/api/services/<domain>/<service>` to call a service within a specific domain and returns [`Value`](serde_json::Value)
516    ///
517    /// request param does not need to have data, it can be empty, e.g.:
518    /// ```ignore
519    /// json!({})
520    /// ```
521    pub async fn service(
522        &self,
523        ha_url: Option<String>,
524        ha_token: Option<String>,
525        ha_domain: &str,
526        ha_service: &str,
527        request: serde_json::Value,
528        return_response: bool,
529    ) -> anyhow::Result<serde_json::Value> {
530        let vars = globalvars();
531        let url = validate().arg(ha_url).or_else(|_| {
532            vars.url
533                .clone()
534                .ok_or(anyhow::Error::msg("HA_URL is required"))
535        })?;
536        let token = validate().arg(ha_token).or_else(|_| {
537            vars.token
538                .clone()
539                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
540        })?;
541
542        let client = post(
543            url,
544            token,
545            &format!(
546                "/api/services/{ha_domain}/{ha_service}{0}",
547                if return_response {
548                    "?return_response"
549                } else {
550                    ""
551                }
552            ),
553            request,
554        )
555        .await?;
556
557        if !client.status().is_success() {
558            Err(anyhow::Error::msg(client.status()))
559        } else {
560            Ok(client.json::<serde_json::Value>().await?)
561        }
562    }
563
564    /// posts to `/api/template` and renders a HASS template and returns [`String`]
565    pub async fn template(
566        &self,
567        ha_url: Option<String>,
568        ha_token: Option<String>,
569        request: structs::TemplateRequest,
570    ) -> anyhow::Result<String> {
571        let vars = globalvars();
572        let url = validate().arg(ha_url).or_else(|_| {
573            vars.url
574                .clone()
575                .ok_or(anyhow::Error::msg("HA_URL is required"))
576        })?;
577        let token = validate().arg(ha_token).or_else(|_| {
578            vars.token
579                .clone()
580                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
581        })?;
582
583        let client = post(url, token, "/api/template", request)
584            .await?
585            .text()
586            .await?;
587
588        Ok(client)
589    }
590
591    /// posts to `/api/config/core/check_config` and checks the config and returns [`ConfigCheckResponse`](structs::ConfigCheckResponse)
592    pub async fn config_check(
593        &self,
594        ha_url: Option<String>,
595        ha_token: Option<String>,
596    ) -> anyhow::Result<structs::ConfigCheckResponse> {
597        let vars = globalvars();
598        let url = validate().arg(ha_url).or_else(|_| {
599            vars.url
600                .clone()
601                .ok_or(anyhow::Error::msg("HA_URL is required"))
602        })?;
603        let token = validate().arg(ha_token).or_else(|_| {
604            vars.token
605                .clone()
606                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
607        })?;
608
609        let client = post(url, token, "/api/config/core/check_config", json!({})).await?;
610
611        if !client.status().is_success() {
612            Err(anyhow::Error::msg(client.status()))
613        } else {
614            Ok(client.json::<structs::ConfigCheckResponse>().await?)
615        }
616    }
617
618    /// posts to `/api/intent/handle` and handles an Intent and returns a [`String`]
619    ///
620    /// I (Blexyel) am unable to test this function
621    pub async fn intent(
622        &self,
623        ha_url: Option<String>,
624        ha_token: Option<String>,
625        request: serde_json::Value,
626    ) -> anyhow::Result<String> {
627        let vars = globalvars();
628        let url = validate().arg(ha_url).or_else(|_| {
629            vars.url
630                .clone()
631                .ok_or(anyhow::Error::msg("HA_URL is required"))
632        })?;
633        let token = validate().arg(ha_token).or_else(|_| {
634            vars.token
635                .clone()
636                .ok_or(anyhow::Error::msg("HA_TOKEN is required"))
637        })?;
638
639        let client = post(url, token, "/api/intent/handle", request)
640            .await?
641            .text()
642            .await?;
643
644        Ok(client)
645    }
646}
647
648pub fn hass() -> HomeAssistant {
649    HomeAssistant
650}