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