1#[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
74lazy_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
147pub struct HomeAssistant;
150
151impl HomeAssistant {
152 pub fn request(&self) -> &'static HomeAssistantPost {
153 &HomeAssistantPost
154 }
155
156 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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}