Skip to main content

homeassistant_cli/commands/
schema.rs

1pub fn build_schema() -> serde_json::Value {
2    serde_json::json!({
3        "name": "ha",
4        "version": env!("CARGO_PKG_VERSION"),
5        "description": "Home Assistant CLI — agent-friendly with structured output and schema introspection",
6        "global_flags": [
7            {"name": "--profile", "env": "HA_PROFILE", "description": "Config profile to use"},
8            {"name": "--output", "values": ["json", "table", "plain"], "env": "HA_OUTPUT", "description": "Output format (auto: json when piped, table in TTY)"},
9            {"name": "--quiet", "description": "Suppress non-data output"}
10        ],
11        "error_envelope": {"ok": false, "error": {"code": "string", "message": "string"}},
12        "exit_codes": {
13            "0": "success",
14            "1": "general error",
15            "2": "auth/config error",
16            "3": "not found",
17            "4": "connection error",
18            "5": "partial failure (some items in a batch succeeded, some failed)"
19        },
20        "commands": [
21            {
22                "name": "entity get",
23                "description": "Get the current state of an entity",
24                "args": [{"name": "entity_id", "required": true, "description": "Entity ID (e.g. light.living_room)"}],
25                "json_shape": {
26                    "ok": true,
27                    "data": {
28                        "entity_id": "string",
29                        "state": "string",
30                        "attributes": "object",
31                        "last_changed": "ISO 8601 timestamp",
32                        "last_updated": "ISO 8601 timestamp"
33                    }
34                }
35            },
36            {
37                "name": "entity list",
38                "description": "List all entities, optionally filtered by domain, state, or count",
39                "flags": [
40                    {"name": "--domain", "description": "Filter by domain (e.g. light, switch, sensor)"},
41                    {"name": "--state", "description": "Filter by state value (e.g. on, off, unavailable)"},
42                    {"name": "--limit", "description": "Maximum number of entities to return"}
43                ],
44                "json_shape": {
45                    "ok": true,
46                    "data": [{"entity_id": "string", "state": "string", "attributes": "object", "last_changed": "string", "last_updated": "string"}]
47                }
48            },
49            {
50                "name": "entity watch",
51                "description": "Stream state changes for an entity (SSE, runs until Ctrl+C)",
52                "args": [{"name": "entity_id", "required": true}],
53                "json_shape": {
54                    "ok": true,
55                    "data": {"entity_id": "string", "new_state": "EntityState | null", "old_state": "EntityState | null"}
56                }
57            },
58            {
59                "name": "service call",
60                "description": "Call a Home Assistant service",
61                "args": [{"name": "service", "required": true, "description": "Service in domain.service format (e.g. light.turn_on)"}],
62                "flags": [
63                    {"name": "--entity", "description": "Target entity ID"},
64                    {"name": "--data", "description": "Additional service data as JSON string"}
65                ],
66                "json_shape": {"ok": true, "data": "array of affected states"}
67            },
68            {
69                "name": "service list",
70                "description": "List available services",
71                "flags": [{"name": "--domain", "description": "Filter by domain"}],
72                "json_shape": {
73                    "ok": true,
74                    "data": [{"domain": "string", "services": {"service_name": {"name": "string", "description": "string"}}}]
75                }
76            },
77            {
78                "name": "event fire",
79                "description": "Fire a Home Assistant event",
80                "args": [{"name": "event_type", "required": true}],
81                "flags": [{"name": "--data", "description": "Event data as JSON string"}],
82                "json_shape": {"ok": true, "data": {"message": "string"}}
83            },
84            {
85                "name": "event watch",
86                "description": "Stream Home Assistant events (SSE, runs until Ctrl+C)",
87                "args": [{"name": "event_type", "required": false, "description": "Filter by event type"}],
88                "json_shape": {
89                    "ok": true,
90                    "data": {"event_type": "string", "data": "object", "time_fired": "ISO 8601 timestamp"}
91                }
92            },
93            {
94                "name": "registry entity list",
95                "description": "List registered entities from the Home Assistant entity registry (WebSocket API)",
96                "flags": [
97                    {"name": "--integration", "description": "Filter by integration/platform (e.g. hue, zha)"},
98                    {"name": "--domain", "description": "Filter by domain (e.g. light, switch)"}
99                ],
100                "json_shape": {
101                    "ok": true,
102                    "data": [{
103                        "entity_id": "string",
104                        "platform": "string",
105                        "name": "string | null",
106                        "original_name": "string | null",
107                        "disabled_by": "string | null",
108                        "area_id": "string | null",
109                        "device_id": "string | null"
110                    }]
111                }
112            },
113            {
114                "name": "registry entity remove",
115                "description": "Permanently remove entities from the entity registry. --dry-run never connects. In a TTY, requires --yes to bypass the confirmation prompt.",
116                "args": [{"name": "entity_ids", "required": true, "description": "One or more entity IDs to remove"}],
117                "flags": [
118                    {"name": "--dry-run", "description": "Print what would be removed without connecting to Home Assistant"},
119                    {"name": "--yes", "description": "Skip the interactive confirmation prompt"}
120                ],
121                "exit_codes": {"5": "one or more removals failed; see per-entity status in data[]"},
122                "json_shape": {
123                    "ok": "bool (true only when every removal succeeded)",
124                    "data": [{
125                        "entity_id": "string",
126                        "status": "removed | not_found | error | dry_run",
127                        "error": "string (only present when status is not_found or error)"
128                    }]
129                }
130            },
131            {
132                "name": "init",
133                "description": "Set up credentials interactively. When stdout is not a TTY, prints JSON setup instructions.",
134                "flags": [{"name": "--profile", "description": "Profile to create or update"}]
135            },
136            {
137                "name": "config show",
138                "description": "Show current configuration and active profile"
139            },
140            {
141                "name": "config set",
142                "description": "Set a config value in the active profile",
143                "args": [
144                    {"name": "key", "required": true, "description": "Config key: url or token"},
145                    {"name": "value", "required": true}
146                ]
147            },
148            {
149                "name": "schema",
150                "description": "Print this machine-readable schema. Use for agent introspection.",
151                "json_shape": "this document"
152            },
153            {
154                "name": "completions",
155                "description": "Generate shell completions",
156                "args": [{"name": "shell", "required": true, "values": ["bash", "zsh", "fish", "elvish", "powershell"]}]
157            }
158        ]
159    })
160}
161
162pub fn print_schema() {
163    println!(
164        "{}",
165        serde_json::to_string_pretty(&build_schema()).expect("serialize")
166    );
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn schema_is_valid_json() {
175        let schema = build_schema();
176        assert!(schema.is_object());
177    }
178
179    #[test]
180    fn schema_has_expected_commands() {
181        let schema = build_schema();
182        let commands = schema["commands"].as_array().unwrap();
183        let names: Vec<&str> = commands
184            .iter()
185            .map(|c| c["name"].as_str().unwrap())
186            .collect();
187        assert!(names.contains(&"entity get"));
188        assert!(names.contains(&"entity list"));
189        assert!(names.contains(&"entity watch"));
190        assert!(names.contains(&"service call"));
191        assert!(names.contains(&"service list"));
192        assert!(names.contains(&"event fire"));
193        assert!(names.contains(&"event watch"));
194        assert!(names.contains(&"registry entity list"));
195        assert!(names.contains(&"registry entity remove"));
196        assert!(names.contains(&"schema"));
197        assert!(names.contains(&"init"));
198        assert!(names.contains(&"config show"));
199        assert!(names.contains(&"config set"));
200    }
201
202    #[test]
203    fn schema_entity_get_has_json_shape() {
204        let schema = build_schema();
205        let commands = schema["commands"].as_array().unwrap();
206        let entity_get = commands.iter().find(|c| c["name"] == "entity get").unwrap();
207        assert!(entity_get["json_shape"]["data"]["entity_id"].is_string());
208        assert!(entity_get["json_shape"]["data"]["state"].is_string());
209    }
210
211    #[test]
212    fn schema_includes_global_flags() {
213        let schema = build_schema();
214        let globals = schema["global_flags"].as_array().unwrap();
215        let flag_names: Vec<&str> = globals
216            .iter()
217            .map(|f| f["name"].as_str().unwrap())
218            .collect();
219        assert!(flag_names.contains(&"--output"));
220        assert!(flag_names.contains(&"--profile"));
221        assert!(flag_names.contains(&"--quiet"));
222    }
223}