homeassistant-cli 0.2.0

Agent-friendly Home Assistant CLI with JSON output, structured exit codes, and schema introspection
Documentation
use clap::{CommandFactory, Parser, Subcommand};

use homeassistant_cli::output::{OutputConfig, OutputFormat, exit_codes};
use homeassistant_cli::{api, commands};

#[derive(Parser)]
#[command(
    name = "ha",
    version,
    about = "CLI for Home Assistant",
    arg_required_else_help = true
)]
struct Cli {
    /// Config profile to use [env: HA_PROFILE]
    #[arg(long, env = "HA_PROFILE", global = true)]
    profile: Option<String>,

    /// Output format [env: HA_OUTPUT]
    #[arg(long, value_enum, env = "HA_OUTPUT", global = true)]
    output: Option<OutputFormat>,

    /// Suppress non-data output
    #[arg(long, global = true)]
    quiet: bool,

    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Read and watch entity states
    #[command(subcommand, arg_required_else_help = true)]
    Entity(EntityCommand),

    /// Call and list services
    #[command(subcommand, arg_required_else_help = true)]
    Service(ServiceCommand),

    /// Fire and watch events
    #[command(subcommand, arg_required_else_help = true)]
    Event(EventCommand),

    /// Manage the Home Assistant entity/device/area registry (WebSocket API)
    #[command(subcommand, arg_required_else_help = true)]
    Registry(RegistryCommand),

    /// Set up credentials interactively (or print JSON schema for agents)
    Init {
        #[arg(long)]
        profile: Option<String>,
    },

    /// Manage configuration
    #[command(subcommand, arg_required_else_help = true)]
    Config(ConfigCommand),

    /// Print machine-readable schema of all commands
    Schema,

    /// Generate shell completions
    Completions {
        /// Shell to generate completions for
        shell: clap_complete::Shell,
    },
}

#[derive(Subcommand)]
enum EntityCommand {
    /// Get the current state of an entity
    Get { entity_id: String },
    /// List all entities, optionally filtered by domain, state, or count
    List {
        #[arg(long)]
        domain: Option<String>,
        /// Filter by state value (e.g. on, off, unavailable)
        #[arg(long)]
        state: Option<String>,
        /// Maximum number of results to show
        #[arg(long)]
        limit: Option<usize>,
    },
    /// Stream state changes for an entity
    Watch { entity_id: String },
}

#[derive(Subcommand)]
enum ServiceCommand {
    /// Call a service
    Call {
        /// Service in domain.service format (e.g. light.turn_on)
        service: String,
        /// Target entity ID
        #[arg(long)]
        entity: Option<String>,
        /// Additional service data as JSON
        #[arg(long)]
        data: Option<String>,
    },
    /// List available services
    List {
        #[arg(long)]
        domain: Option<String>,
    },
}

#[derive(Subcommand)]
enum EventCommand {
    /// Fire an event
    Fire {
        event_type: String,
        /// Event data as JSON
        #[arg(long)]
        data: Option<String>,
    },
    /// Stream events
    Watch {
        /// Filter by event type
        event_type: Option<String>,
    },
}

#[derive(Subcommand)]
enum ConfigCommand {
    /// Show current configuration
    Show,
    /// Set a config value
    Set { key: String, value: String },
}

#[derive(Subcommand)]
enum RegistryCommand {
    /// Entity registry operations
    #[command(subcommand, arg_required_else_help = true)]
    Entity(RegistryEntityCommand),
}

#[derive(Subcommand)]
enum RegistryEntityCommand {
    /// List registered entities
    List {
        /// Filter by integration/platform (e.g. hue, zha)
        #[arg(long)]
        integration: Option<String>,
        /// Filter by domain (e.g. light, switch)
        #[arg(long)]
        domain: Option<String>,
    },
    /// Remove entities from the registry. Requires --yes in interactive mode.
    Remove {
        /// Entity IDs to remove (one or more)
        #[arg(required = true)]
        entity_ids: Vec<String>,
        /// Print what would be removed without connecting to Home Assistant
        #[arg(long)]
        dry_run: bool,
        /// Skip the interactive confirmation prompt
        #[arg(long)]
        yes: bool,
    },
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();
    let out = OutputConfig::new(cli.output, cli.quiet);

    match cli.command {
        Command::Init { profile } => {
            commands::init::init(profile).await;
        }
        Command::Schema => {
            commands::schema::print_schema();
        }
        Command::Completions { shell } => {
            clap_complete::generate(shell, &mut Cli::command(), "ha", &mut std::io::stdout());
        }
        Command::Config(cmd) => match cmd {
            ConfigCommand::Show => {
                commands::config::show(&out, cli.profile.as_deref());
            }
            ConfigCommand::Set { key, value } => {
                commands::config::set(&out, cli.profile.as_deref(), &key, &value);
            }
        },
        command => {
            let cfg = match homeassistant_cli::config::Config::load(cli.profile.clone()) {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("{e}");
                    std::process::exit(exit_codes::CONFIG_ERROR);
                }
            };
            let client = api::HaClient::new(&cfg.url, &cfg.token);

            let result = match command {
                Command::Entity(cmd) => match cmd {
                    EntityCommand::Get { entity_id } => {
                        commands::entity::get(&out, &client, &entity_id).await
                    }
                    EntityCommand::List {
                        domain,
                        state,
                        limit,
                    } => {
                        commands::entity::list(
                            &out,
                            &client,
                            domain.as_deref(),
                            state.as_deref(),
                            limit,
                        )
                        .await
                    }
                    EntityCommand::Watch { entity_id } => {
                        commands::entity::watch(&out, &client, &entity_id).await
                    }
                },
                Command::Service(cmd) => match cmd {
                    ServiceCommand::Call {
                        service,
                        entity,
                        data,
                    } => {
                        commands::service::call(
                            &out,
                            &client,
                            &service,
                            entity.as_deref(),
                            data.as_deref(),
                        )
                        .await
                    }
                    ServiceCommand::List { domain } => {
                        commands::service::list(&out, &client, domain.as_deref()).await
                    }
                },
                Command::Event(cmd) => match cmd {
                    EventCommand::Fire { event_type, data } => {
                        commands::event::fire(&out, &client, &event_type, data.as_deref()).await
                    }
                    EventCommand::Watch { event_type } => {
                        commands::event::watch(&out, &client, event_type.as_deref()).await
                    }
                },
                Command::Registry(cmd) => match cmd {
                    RegistryCommand::Entity(sub) => match sub {
                        RegistryEntityCommand::List {
                            integration,
                            domain,
                        } => {
                            commands::registry::entity_list(
                                &out,
                                &cfg.url,
                                &cfg.token,
                                integration.as_deref(),
                                domain.as_deref(),
                            )
                            .await
                        }
                        RegistryEntityCommand::Remove {
                            entity_ids,
                            dry_run,
                            yes,
                        } => {
                            commands::registry::entity_remove(
                                &out,
                                &cfg.url,
                                &cfg.token,
                                &entity_ids,
                                dry_run,
                                yes,
                            )
                            .await
                        }
                    },
                },
                Command::Init { .. }
                | Command::Schema
                | Command::Config(_)
                | Command::Completions { .. } => unreachable!(),
            };

            if let Err(e) = result {
                let code = exit_codes::for_error(&e);
                out.print_error(&e);
                std::process::exit(code);
            }
        }
    }
}