homeassistant_cli/commands/
schema.rs1pub 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 },
19 "commands": [
20 {
21 "name": "entity get",
22 "description": "Get the current state of an entity",
23 "args": [{"name": "entity_id", "required": true, "description": "Entity ID (e.g. light.living_room)"}],
24 "json_shape": {
25 "ok": true,
26 "data": {
27 "entity_id": "string",
28 "state": "string",
29 "attributes": "object",
30 "last_changed": "ISO 8601 timestamp",
31 "last_updated": "ISO 8601 timestamp"
32 }
33 }
34 },
35 {
36 "name": "entity list",
37 "description": "List all entities, optionally filtered by domain, state, or count",
38 "flags": [
39 {"name": "--domain", "description": "Filter by domain (e.g. light, switch, sensor)"},
40 {"name": "--state", "description": "Filter by state value (e.g. on, off, unavailable)"},
41 {"name": "--limit", "description": "Maximum number of entities to return"}
42 ],
43 "json_shape": {
44 "ok": true,
45 "data": [{"entity_id": "string", "state": "string", "attributes": "object", "last_changed": "string", "last_updated": "string"}]
46 }
47 },
48 {
49 "name": "entity watch",
50 "description": "Stream state changes for an entity (SSE, runs until Ctrl+C)",
51 "args": [{"name": "entity_id", "required": true}],
52 "json_shape": {
53 "ok": true,
54 "data": {"entity_id": "string", "new_state": "EntityState | null", "old_state": "EntityState | null"}
55 }
56 },
57 {
58 "name": "service call",
59 "description": "Call a Home Assistant service",
60 "args": [{"name": "service", "required": true, "description": "Service in domain.service format (e.g. light.turn_on)"}],
61 "flags": [
62 {"name": "--entity", "description": "Target entity ID"},
63 {"name": "--data", "description": "Additional service data as JSON string"}
64 ],
65 "json_shape": {"ok": true, "data": "array of affected states"}
66 },
67 {
68 "name": "service list",
69 "description": "List available services",
70 "flags": [{"name": "--domain", "description": "Filter by domain"}],
71 "json_shape": {
72 "ok": true,
73 "data": [{"domain": "string", "services": {"service_name": {"name": "string", "description": "string"}}}]
74 }
75 },
76 {
77 "name": "event fire",
78 "description": "Fire a Home Assistant event",
79 "args": [{"name": "event_type", "required": true}],
80 "flags": [{"name": "--data", "description": "Event data as JSON string"}],
81 "json_shape": {"ok": true, "data": {"message": "string"}}
82 },
83 {
84 "name": "event watch",
85 "description": "Stream Home Assistant events (SSE, runs until Ctrl+C)",
86 "args": [{"name": "event_type", "required": false, "description": "Filter by event type"}],
87 "json_shape": {
88 "ok": true,
89 "data": {"event_type": "string", "data": "object", "time_fired": "ISO 8601 timestamp"}
90 }
91 },
92 {
93 "name": "init",
94 "description": "Set up credentials interactively. When stdout is not a TTY, prints JSON setup instructions.",
95 "flags": [{"name": "--profile", "description": "Profile to create or update"}]
96 },
97 {
98 "name": "config show",
99 "description": "Show current configuration and active profile"
100 },
101 {
102 "name": "config set",
103 "description": "Set a config value in the active profile",
104 "args": [
105 {"name": "key", "required": true, "description": "Config key: url or token"},
106 {"name": "value", "required": true}
107 ]
108 },
109 {
110 "name": "schema",
111 "description": "Print this machine-readable schema. Use for agent introspection.",
112 "json_shape": "this document"
113 },
114 {
115 "name": "completions",
116 "description": "Generate shell completions",
117 "args": [{"name": "shell", "required": true, "values": ["bash", "zsh", "fish", "elvish", "powershell"]}]
118 }
119 ]
120 })
121}
122
123pub fn print_schema() {
124 println!(
125 "{}",
126 serde_json::to_string_pretty(&build_schema()).expect("serialize")
127 );
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn schema_is_valid_json() {
136 let schema = build_schema();
137 assert!(schema.is_object());
138 }
139
140 #[test]
141 fn schema_has_expected_commands() {
142 let schema = build_schema();
143 let commands = schema["commands"].as_array().unwrap();
144 let names: Vec<&str> = commands
145 .iter()
146 .map(|c| c["name"].as_str().unwrap())
147 .collect();
148 assert!(names.contains(&"entity get"));
149 assert!(names.contains(&"entity list"));
150 assert!(names.contains(&"entity watch"));
151 assert!(names.contains(&"service call"));
152 assert!(names.contains(&"service list"));
153 assert!(names.contains(&"event fire"));
154 assert!(names.contains(&"event watch"));
155 assert!(names.contains(&"schema"));
156 assert!(names.contains(&"init"));
157 assert!(names.contains(&"config show"));
158 assert!(names.contains(&"config set"));
159 }
160
161 #[test]
162 fn schema_entity_get_has_json_shape() {
163 let schema = build_schema();
164 let commands = schema["commands"].as_array().unwrap();
165 let entity_get = commands.iter().find(|c| c["name"] == "entity get").unwrap();
166 assert!(entity_get["json_shape"]["data"]["entity_id"].is_string());
167 assert!(entity_get["json_shape"]["data"]["state"].is_string());
168 }
169
170 #[test]
171 fn schema_includes_global_flags() {
172 let schema = build_schema();
173 let globals = schema["global_flags"].as_array().unwrap();
174 let flag_names: Vec<&str> = globals
175 .iter()
176 .map(|f| f["name"].as_str().unwrap())
177 .collect();
178 assert!(flag_names.contains(&"--output"));
179 assert!(flag_names.contains(&"--profile"));
180 assert!(flag_names.contains(&"--quiet"));
181 }
182}