Skip to main content

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