homeassistant_cli/api/
services.rs1use crate::api::{HaClient, HaError, ServiceDomain};
2
3pub async fn list_services(client: &HaClient) -> Result<Vec<ServiceDomain>, HaError> {
4 let resp = client.get("/api/services").send().await?;
5 match resp.status().as_u16() {
6 200 => Ok(resp.json().await?),
7 401 | 403 => Err(HaError::Auth("Unauthorized".into())),
8 status => Err(HaError::Api {
9 status,
10 message: resp.text().await.unwrap_or_default(),
11 }),
12 }
13}
14
15pub async fn call_service(
16 client: &HaClient,
17 domain: &str,
18 service: &str,
19 data: Option<&serde_json::Value>,
20) -> Result<serde_json::Value, HaError> {
21 let req = client.post(&format!("/api/services/{domain}/{service}"));
22 let req = if let Some(d) = data { req.json(d) } else { req };
23 let resp = req.send().await?;
24 match resp.status().as_u16() {
25 200 => Ok(resp.json().await?),
26 401 | 403 => Err(HaError::Auth("Unauthorized".into())),
27 404 => Err(HaError::NotFound(format!(
28 "Service '{domain}.{service}' not found"
29 ))),
30 status => Err(HaError::Api {
31 status,
32 message: resp.text().await.unwrap_or_default(),
33 }),
34 }
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use crate::api::HaClient;
41 use wiremock::matchers::{method, path};
42 use wiremock::{Mock, MockServer, ResponseTemplate};
43
44 #[tokio::test]
45 async fn list_services_returns_domains() {
46 let server = MockServer::start().await;
47 Mock::given(method("GET"))
48 .and(path("/api/services"))
49 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
50 {
51 "domain": "light",
52 "services": {
53 "turn_on": {"name": "Turn on", "description": "Turn on a light"},
54 "turn_off": {"name": "Turn off", "description": "Turn off a light"}
55 }
56 }
57 ])))
58 .mount(&server)
59 .await;
60
61 let client = HaClient::new(server.uri(), "tok");
62 let domains = list_services(&client).await.unwrap();
63 assert_eq!(domains.len(), 1);
64 assert_eq!(domains[0].domain, "light");
65 assert!(domains[0].services.contains_key("turn_on"));
66 }
67
68 #[tokio::test]
69 async fn call_service_sends_post_with_data() {
70 let server = MockServer::start().await;
71 Mock::given(method("POST"))
72 .and(path("/api/services/light/turn_on"))
73 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
74 .mount(&server)
75 .await;
76
77 let client = HaClient::new(server.uri(), "tok");
78 let result = call_service(
79 &client,
80 "light",
81 "turn_on",
82 Some(&serde_json::json!({"entity_id": "light.living_room"})),
83 )
84 .await;
85 assert!(result.is_ok());
86 }
87
88 #[tokio::test]
89 async fn call_service_returns_not_found_on_404() {
90 let server = MockServer::start().await;
91 Mock::given(method("POST"))
92 .and(path("/api/services/fake/service"))
93 .respond_with(ResponseTemplate::new(404))
94 .mount(&server)
95 .await;
96
97 let client = HaClient::new(server.uri(), "tok");
98 let result = call_service(&client, "fake", "service", None).await;
99 assert!(matches!(result, Err(crate::api::HaError::NotFound(_))));
100 }
101}