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