1#[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
80lazy_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
153pub struct HomeAssistant;
156
157impl HomeAssistant {
158 pub fn request(&self) -> &'static HomeAssistantPost {
159 &HomeAssistantPost
160 }
161
162 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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}