homeassistant-cli 0.1.14

Agent-friendly Home Assistant CLI with JSON output, structured exit codes, and schema introspection
Documentation
use crate::api::{EntityState, HaClient, HaError};

pub async fn get_state(client: &HaClient, entity_id: &str) -> Result<EntityState, HaError> {
    let resp = client
        .get(&format!("/api/states/{entity_id}"))
        .send()
        .await?;
    match resp.status().as_u16() {
        200 => Ok(resp.json().await?),
        401 | 403 => Err(HaError::Auth(format!("Unauthorized accessing {entity_id}"))),
        404 => Err(HaError::NotFound(format!("Entity '{entity_id}' not found"))),
        status => Err(HaError::Api {
            status,
            message: resp.text().await.unwrap_or_default(),
        }),
    }
}

pub async fn list_states(client: &HaClient) -> Result<Vec<EntityState>, HaError> {
    let resp = client.get("/api/states").send().await?;
    match resp.status().as_u16() {
        200 => Ok(resp.json().await?),
        401 | 403 => Err(HaError::Auth("Unauthorized".into())),
        status => Err(HaError::Api {
            status,
            message: resp.text().await.unwrap_or_default(),
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::api::HaClient;
    use wiremock::matchers::{header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    async fn mock_client(server: &MockServer) -> HaClient {
        HaClient::new(server.uri(), "test-token")
    }

    #[tokio::test]
    async fn get_state_returns_entity() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/states/light.living_room"))
            .and(header("Authorization", "Bearer test-token"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "entity_id": "light.living_room",
                "state": "on",
                "attributes": {"brightness": 128},
                "last_changed": "2026-01-01T00:00:00Z",
                "last_updated": "2026-01-01T00:00:00Z"
            })))
            .mount(&server)
            .await;

        let client = mock_client(&server).await;
        let state = get_state(&client, "light.living_room").await.unwrap();
        assert_eq!(state.entity_id, "light.living_room");
        assert_eq!(state.state, "on");
    }

    #[tokio::test]
    async fn get_state_returns_not_found_on_404() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/states/light.missing"))
            .respond_with(ResponseTemplate::new(404))
            .mount(&server)
            .await;

        let client = mock_client(&server).await;
        let result = get_state(&client, "light.missing").await;
        assert!(matches!(result, Err(crate::api::HaError::NotFound(_))));
    }

    #[tokio::test]
    async fn list_states_returns_all_entities() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/states"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
                {"entity_id": "light.x", "state": "on", "attributes": {}, "last_changed": "2026-01-01T00:00:00Z", "last_updated": "2026-01-01T00:00:00Z"},
                {"entity_id": "switch.y", "state": "off", "attributes": {}, "last_changed": "2026-01-01T00:00:00Z", "last_updated": "2026-01-01T00:00:00Z"}
            ])))
            .mount(&server)
            .await;

        let client = mock_client(&server).await;
        let states = list_states(&client).await.unwrap();
        assert_eq!(states.len(), 2);
    }

    #[tokio::test]
    async fn get_state_returns_auth_error_on_401() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/states/light.x"))
            .respond_with(ResponseTemplate::new(401))
            .mount(&server)
            .await;

        let client = mock_client(&server).await;
        let result = get_state(&client, "light.x").await;
        assert!(matches!(result, Err(crate::api::HaError::Auth(_))));
    }
}