Skip to main content

homeassistant_cli/commands/
entity.rs

1use owo_colors::OwoColorize;
2
3use crate::api::{self, HaClient, HaError};
4use crate::output::{self, OutputConfig};
5
6pub async fn get(out: &OutputConfig, client: &HaClient, entity_id: &str) -> Result<(), HaError> {
7    let state = api::entities::get_state(client, entity_id).await?;
8
9    if out.is_json() {
10        out.print_data(
11            &serde_json::to_string_pretty(&serde_json::json!({
12                "ok": true,
13                "data": state
14            }))
15            .expect("serialize"),
16        );
17    } else {
18        let attrs = state
19            .attributes
20            .as_object()
21            .map(|m| {
22                m.iter()
23                    .map(|(k, v)| format!("{}={}", k, v))
24                    .collect::<Vec<_>>()
25                    .join("  ")
26            })
27            .unwrap_or_default();
28        let status_sym = if state.state == "on" {
29            "●".green().to_string()
30        } else {
31            "○".dimmed().to_string()
32        };
33        out.print_data(&format!(
34            "{} {}  {}  {}",
35            status_sym,
36            state.entity_id,
37            state.state.bold(),
38            attrs.dimmed()
39        ));
40    }
41    Ok(())
42}
43
44pub async fn list(
45    out: &OutputConfig,
46    client: &HaClient,
47    domain: Option<&str>,
48    state_filter: Option<&str>,
49    limit: Option<usize>,
50) -> Result<(), HaError> {
51    let mut states = api::entities::list_states(client).await?;
52
53    if let Some(d) = domain {
54        states.retain(|s| s.entity_id.starts_with(&format!("{d}.")));
55    }
56    if let Some(st) = state_filter {
57        states.retain(|s| s.state == st);
58    }
59
60    states.sort_by(|a, b| a.entity_id.cmp(&b.entity_id));
61
62    if let Some(n) = limit {
63        states.truncate(n);
64    }
65
66    if out.is_json() {
67        out.print_data(
68            &serde_json::to_string_pretty(&serde_json::json!({
69                "ok": true,
70                "data": states
71            }))
72            .expect("serialize"),
73        );
74    } else {
75        let rows: Vec<Vec<String>> = states
76            .iter()
77            .map(|s| {
78                let name = s
79                    .attributes
80                    .get("friendly_name")
81                    .and_then(|v| v.as_str())
82                    .unwrap_or("")
83                    .to_owned();
84                vec![
85                    output::colored_entity_id(&s.entity_id),
86                    name,
87                    output::colored_state(&s.state),
88                    output::relative_time(&s.last_updated),
89                ]
90            })
91            .collect();
92        out.print_data(&output::table(
93            &["ENTITY", "NAME", "STATE", "UPDATED"],
94            &rows,
95        ));
96    }
97    Ok(())
98}
99
100pub async fn watch(out: &OutputConfig, client: &HaClient, entity_id: &str) -> Result<(), HaError> {
101    out.print_message(&format!("Watching {} (Ctrl+C to stop)...", entity_id));
102
103    let entity_id = entity_id.to_owned();
104    api::events::watch_stream(client, Some("state_changed"), |event| {
105        if let Ok(data) = serde_json::from_value::<crate::api::StateChangedData>(event.data.clone())
106            && data.entity_id == entity_id
107        {
108            if out.is_json() {
109                if let Ok(s) = serde_json::to_string_pretty(&serde_json::json!({
110                    "ok": true,
111                    "data": data
112                })) {
113                    println!("{s}");
114                }
115            } else if let Some(new) = &data.new_state {
116                let status_sym = if new.state == "on" {
117                    "●".green().to_string()
118                } else {
119                    "○".dimmed().to_string()
120                };
121                println!(
122                    "{} {}  {}  {}",
123                    status_sym,
124                    new.entity_id,
125                    new.state.bold(),
126                    output::relative_time(&new.last_updated).dimmed()
127                );
128            }
129        }
130        true
131    })
132    .await
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::api::HaClient;
139    use crate::output::{OutputConfig, OutputFormat};
140    use wiremock::matchers::{method, path};
141    use wiremock::{Mock, MockServer, ResponseTemplate};
142
143    fn json_out() -> OutputConfig {
144        OutputConfig::new(Some(OutputFormat::Json), false)
145    }
146
147    fn state_json(entity_id: &str, state: &str) -> serde_json::Value {
148        serde_json::json!({
149            "entity_id": entity_id,
150            "state": state,
151            "attributes": {},
152            "last_changed": "2026-01-01T00:00:00Z",
153            "last_updated": "2026-01-01T00:00:00Z"
154        })
155    }
156
157    #[tokio::test]
158    async fn get_returns_ok_for_existing_entity() {
159        let server = MockServer::start().await;
160        Mock::given(method("GET"))
161            .and(path("/api/states/light.x"))
162            .respond_with(ResponseTemplate::new(200).set_body_json(state_json("light.x", "on")))
163            .mount(&server)
164            .await;
165
166        let client = HaClient::new(server.uri(), "tok");
167        let result = get(&json_out(), &client, "light.x").await;
168        assert!(result.is_ok());
169    }
170
171    #[tokio::test]
172    async fn list_returns_ok() {
173        let server = MockServer::start().await;
174        Mock::given(method("GET"))
175            .and(path("/api/states"))
176            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
177                state_json("light.a", "on"),
178                state_json("switch.b", "off"),
179                state_json("light.c", "off"),
180            ])))
181            .mount(&server)
182            .await;
183
184        let client = HaClient::new(server.uri(), "tok");
185        let result = list(&json_out(), &client, Some("light"), None, None).await;
186        assert!(result.is_ok());
187    }
188
189    #[tokio::test]
190    async fn get_propagates_not_found() {
191        let server = MockServer::start().await;
192        Mock::given(method("GET"))
193            .and(path("/api/states/light.missing"))
194            .respond_with(ResponseTemplate::new(404))
195            .mount(&server)
196            .await;
197
198        let client = HaClient::new(server.uri(), "tok");
199        let result = get(&json_out(), &client, "light.missing").await;
200        assert!(matches!(result, Err(crate::api::HaError::NotFound(_))));
201    }
202}