mod noise;
mod plain;
mod stream_reader;
mod stream_writer;
use std::{fmt::Debug, time::Duration};
use stream_reader::StreamReader;
use stream_writer::StreamWriter;
use tokio::time::timeout;
use crate::{
API_VERSION,
error::{ClientError, ProtocolError},
proto::{DisconnectRequest, EspHomeMessage, HelloRequest, PingResponse},
};
type StreamPair = (StreamReader, StreamWriter);
#[derive(Debug)]
pub struct EspHomeClient {
streams: StreamPair,
handle_ping: bool,
}
impl EspHomeClient {
#[must_use]
pub fn builder() -> EspHomeClientBuilder {
EspHomeClientBuilder::new()
}
pub async fn try_write<M>(&mut self, message: M) -> Result<(), ClientError>
where
M: Into<EspHomeMessage> + Debug,
{
tracing::debug!("Send: {message:?}");
let message: EspHomeMessage = message.into();
let payload: Vec<u8> = message.into();
self.streams.1.write_message(payload).await
}
pub async fn try_read(&mut self) -> Result<EspHomeMessage, ClientError> {
loop {
let payload = self.streams.0.read_next_message().await?;
let message: EspHomeMessage =
payload
.clone()
.try_into()
.map_err(|e| ProtocolError::ValidationFailed {
reason: format!("Failed to decode EspHomeMessage: {e}"),
})?;
tracing::debug!("Receive: {message:?}");
match message {
EspHomeMessage::PingRequest(_) if self.handle_ping => {
self.try_write(PingResponse {}).await?;
}
msg => return Ok(msg),
}
}
}
pub async fn close(mut self) -> Result<(), ClientError> {
self.try_write(DisconnectRequest {}).await?;
Ok(())
}
#[must_use]
pub fn write_stream(&self) -> EspHomeClientWriteStream {
EspHomeClientWriteStream {
writer: self.streams.1.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct EspHomeClientWriteStream {
writer: StreamWriter,
}
impl EspHomeClientWriteStream {
pub async fn try_write<M>(&self, message: M) -> Result<(), ClientError>
where
M: Into<EspHomeMessage> + Debug,
{
tracing::debug!("Send: {message:?}");
let message: EspHomeMessage = message.into();
let payload: Vec<u8> = message.into();
self.writer.write_message(payload).await
}
}
#[derive(Debug)]
pub struct EspHomeClientBuilder {
addr: Option<String>,
key: Option<String>,
password: Option<String>,
client_info: String,
timeout: Duration,
connection_setup: bool,
handle_ping: bool,
}
impl EspHomeClientBuilder {
fn new() -> Self {
Self {
addr: None,
key: None,
password: None,
client_info: format!("{}:{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
timeout: Duration::from_secs(30),
connection_setup: true,
handle_ping: true,
}
}
#[must_use]
pub fn address(mut self, addr: &str) -> Self {
self.addr = Some(addr.to_owned());
self
}
#[must_use]
pub fn key(mut self, key: &str) -> Self {
self.key = Some(key.to_owned());
self
}
#[must_use]
pub fn password(mut self, password: &str) -> Self {
self.password = Some(password.to_owned());
self
}
#[must_use]
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn client_info(mut self, client_info: &str) -> Self {
client_info.clone_into(&mut self.client_info);
self
}
#[must_use]
pub const fn without_connection_setup(mut self) -> Self {
self.connection_setup = false;
self
}
#[must_use]
pub const fn without_ping_handling(mut self) -> Self {
self.handle_ping = false;
self
}
pub async fn connect(self) -> Result<EspHomeClient, ClientError> {
let addr = self.addr.ok_or_else(|| ClientError::Configuration {
message: "Address is not set".into(),
})?;
let streams = timeout(self.timeout, async {
match self.key {
Some(key) => noise::connect(&addr, &key).await,
None => plain::connect(&addr).await,
}
})
.await
.map_err(|_e| ClientError::Timeout {
timeout_ms: self.timeout.as_millis(),
})??;
let mut stream = EspHomeClient {
streams,
handle_ping: self.handle_ping,
};
if self.connection_setup {
Self::connection_setup(&mut stream, self.client_info, self.password).await?;
}
Ok(stream)
}
async fn connection_setup(
stream: &mut EspHomeClient,
client_info: String,
password: Option<String>,
) -> Result<(), ClientError> {
stream
.try_write(HelloRequest {
client_info,
api_version_major: API_VERSION.0,
api_version_minor: API_VERSION.1,
})
.await?;
loop {
let response = stream.try_read().await?;
match response {
EspHomeMessage::HelloResponse(response) => {
if response.api_version_major != API_VERSION.0 {
return Err(ClientError::ProtocolMismatch {
expected: format!("{}.{}", API_VERSION.0, API_VERSION.1),
actual: format!(
"{}.{}",
response.api_version_major, response.api_version_minor
),
});
}
if response.api_version_minor != API_VERSION.1 {
tracing::warn!(
"API version mismatch: expected {}.{}, got {}.{}, expect breaking changes in messages",
API_VERSION.0,
API_VERSION.1,
response.api_version_major,
response.api_version_minor
);
}
break;
}
_ => {
tracing::debug!("Unexpected response during connection setup: {response:?}");
}
}
}
if password.is_some() {
Self::authenticate(stream, password).await
} else {
Ok(())
}
}
#[cfg(not(any(
feature = "api-1-12",
feature = "api-1-10",
feature = "api-1-9",
feature = "api-1-8"
)))]
async fn authenticate(
stream: &mut EspHomeClient,
password: Option<String>,
) -> Result<(), ClientError> {
use crate::proto::AuthenticationRequest;
stream
.try_write(AuthenticationRequest {
password: password.unwrap_or_default(),
})
.await?;
loop {
let response = stream.try_read().await?;
match response {
EspHomeMessage::AuthenticationResponse(response) => {
if response.invalid_password {
return Err(ClientError::Authentication {
reason: "Invalid password".to_owned(),
});
}
tracing::info!("Connection to ESPHome API established successfully.");
break;
}
_ => {
tracing::debug!("Unexpected response during connection setup: {response:?}");
}
}
}
Ok(())
}
#[cfg(any(
feature = "api-1-12",
feature = "api-1-10",
feature = "api-1-9",
feature = "api-1-8"
))]
async fn authenticate(
stream: &mut EspHomeClient,
password: Option<String>,
) -> Result<(), ClientError> {
use crate::proto::ConnectRequest;
stream
.try_write(ConnectRequest {
password: password.unwrap_or_default(),
})
.await?;
loop {
let response = stream.try_read().await?;
match response {
EspHomeMessage::ConnectResponse(response) => {
if response.invalid_password {
return Err(ClientError::Authentication {
reason: "Invalid password".to_owned(),
});
}
tracing::info!("Connection to ESPHome API established successfully.");
break;
}
_ => {
tracing::debug!("Unexpected response during connection setup: {response:?}");
}
}
}
Ok(())
}
}