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