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 Self::authenticate(stream, password).await
294 }
295
296 #[cfg(not(any(
297 feature = "api-1-12",
298 feature = "api-1-10",
299 feature = "api-1-9",
300 feature = "api-1-8"
301 )))]
302 async fn authenticate(
303 stream: &mut EspHomeClient,
304 password: Option<String>,
305 ) -> Result<(), ClientError> {
306 use crate::proto::AuthenticationRequest;
307
308 stream
309 .try_write(AuthenticationRequest {
310 password: password.unwrap_or_default(),
311 })
312 .await?;
313 loop {
314 let response = stream.try_read().await?;
315 match response {
316 EspHomeMessage::AuthenticationResponse(response) => {
317 if response.invalid_password {
318 return Err(ClientError::Authentication {
319 reason: "Invalid password".to_owned(),
320 });
321 }
322 tracing::info!("Connection to ESPHome API established successfully.");
323 break;
324 }
325 _ => {
326 tracing::debug!("Unexpected response during connection setup: {response:?}");
327 }
328 }
329 }
330 Ok(())
331 }
332
333 #[cfg(any(
334 feature = "api-1-12",
335 feature = "api-1-10",
336 feature = "api-1-9",
337 feature = "api-1-8"
338 ))]
339 async fn authenticate(
340 stream: &mut EspHomeClient,
341 password: Option<String>,
342 ) -> Result<(), ClientError> {
343 use crate::proto::ConnectRequest;
344
345 stream
346 .try_write(ConnectRequest {
347 password: password.unwrap_or_default(),
348 })
349 .await?;
350 loop {
351 let response = stream.try_read().await?;
352 match response {
353 EspHomeMessage::ConnectResponse(response) => {
354 if response.invalid_password {
355 return Err(ClientError::Authentication {
356 reason: "Invalid password".to_owned(),
357 });
358 }
359 tracing::info!("Connection to ESPHome API established successfully.");
360 break;
361 }
362 _ => {
363 tracing::debug!("Unexpected response during connection setup: {response:?}");
364 }
365 }
366 }
367 Ok(())
368 }
369}