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