homeassistant_cli/commands/
event.rs1use std::io::IsTerminal;
2
3use owo_colors::OwoColorize;
4
5use crate::api::{self, HaClient, HaError};
6use crate::output::{self, OutputConfig};
7
8pub async fn fire(
9 out: &OutputConfig,
10 client: &HaClient,
11 event_type: &str,
12 data: Option<&str>,
13 yes: bool,
14) -> Result<(), HaError> {
15 let is_tty = std::io::stdin().is_terminal();
17 if !yes && !out.is_json() {
18 if is_tty {
19 eprint!("Fire event '{event_type}'? [y/N] ");
20 use std::io::Write;
21 let _ = std::io::stderr().flush();
22 let mut input = String::new();
23 std::io::stdin()
24 .read_line(&mut input)
25 .map_err(|e| HaError::Other(format!("failed to read stdin: {e}")))?;
26 let answer = input.trim().to_ascii_lowercase();
27 if answer != "y" && answer != "yes" {
28 return Err(HaError::InvalidInput("aborted by user".into()));
29 }
30 } else {
31 return Err(HaError::ConfirmationRequired(format!(
33 "Firing event '{event_type}' requires confirmation"
34 )));
35 }
36 }
37
38 let body = if let Some(d) = data {
39 Some(
40 serde_json::from_str::<serde_json::Value>(d)
41 .map_err(|e| HaError::InvalidInput(format!("Invalid JSON data: {e}")))?,
42 )
43 } else {
44 None
45 };
46
47 let result = api::events::fire_event(client, event_type, body.as_ref()).await?;
48
49 if out.is_json() {
50 out.print_data(
51 &serde_json::to_string_pretty(&serde_json::json!({"ok": true, "data": result}))
52 .expect("serialize"),
53 );
54 } else {
55 out.print_data(&format!("Fired event: {event_type}"));
56 }
57 Ok(())
58}
59
60pub async fn watch(
61 out: &OutputConfig,
62 client: &HaClient,
63 event_type: Option<&str>,
64) -> Result<(), HaError> {
65 out.print_message(&format!(
66 "Watching events{} (Ctrl+C to stop)...",
67 event_type.map(|t| format!(": {t}")).unwrap_or_default()
68 ));
69
70 api::events::watch_stream(client, event_type, |event| {
71 if out.is_json() {
72 if let Ok(s) =
73 serde_json::to_string_pretty(&serde_json::json!({"ok": true, "data": event}))
74 {
75 println!("{s}");
76 }
77 } else {
78 let time = event
79 .time_fired
80 .as_deref()
81 .map(output::relative_time)
82 .unwrap_or_else(|| "-".to_owned());
83 let data_str = if event.data.is_null() || event.data == serde_json::json!({}) {
84 String::new()
85 } else {
86 format!(" {}", event.data.to_string().dimmed())
87 };
88 println!("{} {}{}", time.dimmed(), event.event_type.bold(), data_str);
89 }
90 true
91 })
92 .await
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::api::HaClient;
99 use crate::output::{OutputConfig, OutputFormat};
100 use wiremock::matchers::{method, path};
101 use wiremock::{Mock, MockServer, ResponseTemplate};
102
103 fn json_out() -> OutputConfig {
104 OutputConfig::new(Some(OutputFormat::Json), false)
105 }
106
107 #[tokio::test]
108 async fn fire_succeeds_on_200() {
109 let server = MockServer::start().await;
110 Mock::given(method("POST"))
111 .and(path("/api/events/my_event"))
112 .respond_with(
113 ResponseTemplate::new(200)
114 .set_body_json(serde_json::json!({"message": "Event my_event fired."})),
115 )
116 .mount(&server)
117 .await;
118
119 let client = HaClient::new(server.uri(), "tok");
120 let result = fire(&json_out(), &client, "my_event", None, true).await;
122 assert!(result.is_ok());
123 }
124
125 #[tokio::test]
126 async fn fire_with_invalid_json_returns_error() {
127 let server = MockServer::start().await;
128 let client = HaClient::new(server.uri(), "tok");
129 let result = fire(&json_out(), &client, "my_event", Some("{invalid}"), true).await;
131 assert!(matches!(result, Err(crate::api::HaError::InvalidInput(_))));
132 }
133
134 #[tokio::test]
135 async fn fire_without_yes_in_non_tty_text_mode_requires_confirmation() {
136 use crate::output::OutputFormat;
137 let text_out = OutputConfig::new(Some(OutputFormat::Text), false);
139 let server = MockServer::start().await;
140 let client = HaClient::new(server.uri(), "tok");
141 let result = fire(&text_out, &client, "my_event", None, false).await;
142 assert!(
143 matches!(result, Err(crate::api::HaError::ConfirmationRequired(_))),
144 "expected ConfirmationRequired, got {result:?}"
145 );
146 }
147}