homeassistant-cli 0.2.0

Agent-friendly Home Assistant CLI with JSON output, structured exit codes, and schema introspection
Documentation
use owo_colors::OwoColorize;

use crate::api::{self, HaClient, HaError};
use crate::output::{self, OutputConfig};

pub async fn fire(
    out: &OutputConfig,
    client: &HaClient,
    event_type: &str,
    data: Option<&str>,
) -> Result<(), HaError> {
    let body = if let Some(d) = data {
        Some(
            serde_json::from_str::<serde_json::Value>(d)
                .map_err(|e| HaError::InvalidInput(format!("Invalid JSON data: {e}")))?,
        )
    } else {
        None
    };

    let result = api::events::fire_event(client, event_type, body.as_ref()).await?;

    if out.is_json() {
        out.print_data(
            &serde_json::to_string_pretty(&serde_json::json!({"ok": true, "data": result}))
                .expect("serialize"),
        );
    } else {
        out.print_data(&format!("✔ Fired event: {event_type}"));
    }
    Ok(())
}

pub async fn watch(
    out: &OutputConfig,
    client: &HaClient,
    event_type: Option<&str>,
) -> Result<(), HaError> {
    out.print_message(&format!(
        "Watching events{} (Ctrl+C to stop)...",
        event_type.map(|t| format!(": {t}")).unwrap_or_default()
    ));

    api::events::watch_stream(client, event_type, |event| {
        if out.is_json() {
            if let Ok(s) =
                serde_json::to_string_pretty(&serde_json::json!({"ok": true, "data": event}))
            {
                println!("{s}");
            }
        } else {
            let time = event
                .time_fired
                .as_deref()
                .map(output::relative_time)
                .unwrap_or_else(|| "-".to_owned());
            let data_str = if event.data.is_null() || event.data == serde_json::json!({}) {
                String::new()
            } else {
                format!("  {}", event.data.to_string().dimmed())
            };
            println!("{}  {}{}", time.dimmed(), event.event_type.bold(), data_str);
        }
        true
    })
    .await
}

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

    fn json_out() -> OutputConfig {
        OutputConfig::new(Some(OutputFormat::Json), false)
    }

    #[tokio::test]
    async fn fire_succeeds_on_200() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/api/events/my_event"))
            .respond_with(
                ResponseTemplate::new(200)
                    .set_body_json(serde_json::json!({"message": "Event my_event fired."})),
            )
            .mount(&server)
            .await;

        let client = HaClient::new(server.uri(), "tok");
        let result = fire(&json_out(), &client, "my_event", None).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn fire_with_invalid_json_returns_error() {
        let server = MockServer::start().await;
        let client = HaClient::new(server.uri(), "tok");
        let result = fire(&json_out(), &client, "my_event", Some("{invalid}")).await;
        assert!(matches!(result, Err(crate::api::HaError::InvalidInput(_))));
    }
}