Skip to main content

homeassistant_cli/api/
services.rs

1use 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}