1mod noise;
8mod plain;
9
10mod stream_reader;
11mod stream_writer;
12use std::{fmt::Debug, time::Duration};
13
14use stream_reader::StreamReader;
15use stream_writer::StreamWriter;
16use tokio::time::timeout;
17
18use crate::{
19 error::{ClientError, ProtocolError},
20 proto::{DisconnectRequest, EspHomeMessage, HelloRequest, PingResponse},
21 API_VERSION,
22};
23
24type StreamPair = (StreamReader, StreamWriter);
25
26#[derive(Debug)]
28pub struct EspHomeClient {
29 streams: StreamPair,
30 handle_ping: bool,
31}
32
33impl EspHomeClient {
34 #[must_use]
36 pub fn builder() -> EspHomeClientBuilder {
37 EspHomeClientBuilder::new()
38 }
39
40 pub async fn try_write<M>(&mut self, message: M) -> Result<(), ClientError>
46 where
47 M: Into<EspHomeMessage> + Debug,
48 {
49 tracing::debug!("Send: {message:?}");
50 let message: EspHomeMessage = message.into();
51 let payload: Vec<u8> = message.into();
52 self.streams.1.write_message(payload).await
53 }
54
55 pub async fn try_read(&mut self) -> Result<EspHomeMessage, ClientError> {
63 loop {
64 let payload = self.streams.0.read_next_message().await?;
65 let message: EspHomeMessage =
66 payload
67 .clone()
68 .try_into()
69 .map_err(|e| ProtocolError::ValidationFailed {
70 reason: format!("Failed to decode EspHomeMessage: {e}"),
71 })?;
72 tracing::debug!("Receive: {message:?}");
73 match message {
74 EspHomeMessage::PingRequest(_) if self.handle_ping => {
75 self.try_write(PingResponse {}).await?;
76 }
77 msg => return Ok(msg),
78 }
79 }
80 }
81
82 pub async fn close(mut self) -> Result<(), ClientError> {
88 self.try_write(DisconnectRequest {}).await?;
89 Ok(())
91 }
92
93 #[must_use]
95 pub fn write_stream(&self) -> EspHomeClientWriteStream {
96 EspHomeClientWriteStream {
97 writer: self.streams.1.clone(),
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct EspHomeClientWriteStream {
105 writer: StreamWriter,
106}
107impl EspHomeClientWriteStream {
108 pub async fn try_write<M>(&self, message: M) -> Result<(), ClientError>
114 where
115 M: Into<EspHomeMessage> + Debug,
116 {
117 tracing::debug!("Send: {message:?}");
118 let message: EspHomeMessage = message.into();
119 let payload: Vec<u8> = message.into();
120 self.writer.write_message(payload).await
121 }
122}
123
124#[derive(Debug)]
126pub struct EspHomeClientBuilder {
127 addr: Option<String>,
128 key: Option<String>,
129 password: Option<String>,
130 client_info: String,
131 timeout: Duration,
132 connection_setup: bool,
133 handle_ping: bool,
134}
135
136impl EspHomeClientBuilder {
137 fn new() -> Self {
138 Self {
139 addr: None,
140 key: None,
141 password: None,
142 client_info: format!("{}:{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
143 timeout: Duration::from_secs(30),
144 connection_setup: true,
145 handle_ping: true,
146 }
147 }
148
149 #[must_use]
153 pub fn address(mut self, addr: &str) -> Self {
154 self.addr = Some(addr.to_owned());
155 self
156 }
157
158 #[must_use]
163 pub fn key(mut self, key: &str) -> Self {
164 self.key = Some(key.to_owned());
165 self
166 }
167
168 #[must_use]
173 pub fn password(mut self, password: &str) -> Self {
174 self.password = Some(password.to_owned());
175 self
176 }
177
178 #[must_use]
180 pub const fn timeout(mut self, timeout: Duration) -> Self {
181 self.timeout = timeout;
182 self
183 }
184
185 #[must_use]
190 pub fn client_info(mut self, client_info: &str) -> Self {
191 client_info.clone_into(&mut self.client_info);
192 self
193 }
194
195 #[must_use]
203 pub const fn without_connection_setup(mut self) -> Self {
204 self.connection_setup = false;
205 self
206 }
207
208 #[must_use]
213 pub const fn without_ping_handling(mut self) -> Self {
214 self.handle_ping = false;
215 self
216 }
217
218 pub async fn connect(self) -> Result<EspHomeClient, ClientError> {
224 let addr = self.addr.ok_or_else(|| ClientError::Configuration {
225 message: "Address is not set".into(),
226 })?;
227
228 let streams = timeout(self.timeout, async {
229 match self.key {
230 Some(key) => noise::connect(&addr, &key).await,
231 None => plain::connect(&addr).await,
232 }
233 })
234 .await
235 .map_err(|_e| ClientError::Timeout {
236 timeout_ms: self.timeout.as_millis(),
237 })??;
238
239 let mut stream = EspHomeClient {
240 streams,
241 handle_ping: self.handle_ping,
242 };
243 if self.connection_setup {
244 Self::connection_setup(&mut stream, self.client_info, self.password).await?;
245 }
246 Ok(stream)
247 }
248
249 async fn connection_setup(
253 stream: &mut EspHomeClient,
254 client_info: String,
255 password: Option<String>,
256 ) -> Result<(), ClientError> {
257 stream
258 .try_write(HelloRequest {
259 client_info,
260 api_version_major: API_VERSION.0,
261 api_version_minor: API_VERSION.1,
262 })
263 .await?;
264 loop {
265 let response = stream.try_read().await?;
266 match response {
267 EspHomeMessage::HelloResponse(response) => {
268 if response.api_version_major != API_VERSION.0 {
269 return Err(ClientError::ProtocolMismatch {
270 expected: format!("{}.{}", API_VERSION.0, API_VERSION.1),
271 actual: format!(
272 "{}.{}",
273 response.api_version_major, response.api_version_minor
274 ),
275 });
276 }
277 if response.api_version_minor != API_VERSION.1 {
278 tracing::warn!(
279 "API version mismatch: expected {}.{}, got {}.{}, expect breaking changes in messages",
280 API_VERSION.0,
281 API_VERSION.1,
282 response.api_version_major,
283 response.api_version_minor
284 );
285 }
286 break;
287 }
288 _ => {
289 tracing::debug!("Unexpected response during connection setup: {response:?}");
290 }
291 }
292 }
293 if password.is_some() {
294 Self::authenticate(stream, password).await
295 } else {
296 Ok(())
297 }
298 }
299
300 #[cfg(not(any(
301 feature = "api-1-12",
302 feature = "api-1-10",
303 feature = "api-1-9",
304 feature = "api-1-8"
305 )))]
306 async fn authenticate(
307 stream: &mut EspHomeClient,
308 password: Option<String>,
309 ) -> Result<(), ClientError> {
310 use crate::proto::AuthenticationRequest;
311
312 stream
313 .try_write(AuthenticationRequest {
314 password: password.unwrap_or_default(),
315 })
316 .await?;
317 loop {
318 let response = stream.try_read().await?;
319 match response {
320 EspHomeMessage::AuthenticationResponse(response) => {
321 if response.invalid_password {
322 return Err(ClientError::Authentication {
323 reason: "Invalid password".to_owned(),
324 });
325 }
326 tracing::info!("Connection to ESPHome API established successfully.");
327 break;
328 }
329 _ => {
330 tracing::debug!("Unexpected response during connection setup: {response:?}");
331 }
332 }
333 }
334 Ok(())
335 }
336
337 #[cfg(any(
338 feature = "api-1-12",
339 feature = "api-1-10",
340 feature = "api-1-9",
341 feature = "api-1-8"
342 ))]
343 async fn authenticate(
344 stream: &mut EspHomeClient,
345 password: Option<String>,
346 ) -> Result<(), ClientError> {
347 use crate::proto::ConnectRequest;
348
349 stream
350 .try_write(ConnectRequest {
351 password: password.unwrap_or_default(),
352 })
353 .await?;
354 loop {
355 let response = stream.try_read().await?;
356 match response {
357 EspHomeMessage::ConnectResponse(response) => {
358 if response.invalid_password {
359 return Err(ClientError::Authentication {
360 reason: "Invalid password".to_owned(),
361 });
362 }
363 tracing::info!("Connection to ESPHome API established successfully.");
364 break;
365 }
366 _ => {
367 tracing::debug!("Unexpected response during connection setup: {response:?}");
368 }
369 }
370 }
371 Ok(())
372 }
373}