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