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 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}