esphome_native_api/esphomeserver.rs
1//! High-level ESPHome server implementation with entity management.
2//!
3//! This module provides the [`EspHomeServer`] abstraction, which simplifies working with
4//! ESPHome devices by managing entities. It builds on top of the
5//! lower-level [`crate::esphomeapi::EspHomeApi`] and handles entity registration and
6//! message routing automatically.
7//!
8//! # Examples
9//!
10//! ```rust,no_run
11//! use esphome_native_api::esphomeserver::{EspHomeServer, Entity, BinarySensor};
12//! use tokio::net::TcpStream;
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! let stream = TcpStream::connect("192.168.1.100:6053").await?;
17//!
18//! let mut server = EspHomeServer::builder()
19//! .name("my-server".to_string())
20//! .build();
21//!
22//! // Add entities
23//! let sensor = Entity::BinarySensor(BinarySensor {
24//! object_id: "door_sensor".to_string(),
25//! });
26//! server.add_entity("door_sensor", sensor);
27//!
28//! let connection = server.start(stream).await?;
29//! let tx = connection.sender();
30//! let mut rx = connection.receiver();
31//! # let _ = (tx, &mut rx);
32//!
33//! Ok(())
34//! }
35//! ```
36
37#![allow(dead_code)]
38
39use log::debug;
40use log::warn;
41use noise_protocol::CipherState;
42use noise_protocol::HandshakeState;
43use noise_rust_crypto::ChaCha20Poly1305;
44use noise_rust_crypto::Sha256;
45use noise_rust_crypto::X25519;
46use std::collections::HashMap;
47use std::str;
48use std::sync::Arc;
49use std::sync::atomic::AtomicBool;
50use tokio::net::TcpStream;
51use tokio::sync::Mutex;
52use tokio::sync::broadcast;
53use tokio::sync::broadcast::error::RecvError;
54use typed_builder::TypedBuilder;
55
56use crate::connection::Connection;
57use crate::error::Error;
58use crate::esphomeapi::EspHomeApi;
59use crate::parser::ProtoMessage;
60use crate::proto::ListEntitiesDoneResponse;
61
62/// High-level ESPHome server implementation.
63///
64/// `EspHomeServer` provides an easier-to-use abstraction over the ESPHome native API
65/// by managing entity keys internally. It handles entity registration, message routing,
66/// and maintains state for all registered entities.
67///
68/// This struct uses the builder pattern via the [`TypedBuilder`] derive macro,
69/// allowing for flexible configuration.
70///
71/// # Examples
72///
73/// ```rust
74/// use esphome_native_api::esphomeserver::EspHomeServer;
75///
76/// let server = EspHomeServer::builder()
77/// .name("my-device".to_string())
78/// .api_version_major(1)
79/// .api_version_minor(10)
80/// .encryption_key("your-base64-key".to_string())
81/// .build();
82/// ```
83#[derive(TypedBuilder)]
84pub struct EspHomeServer {
85 // Private fields
86 #[builder(default=HashMap::new(), setter(skip))]
87 pub(crate) components_by_key: HashMap<u32, Entity>,
88 #[builder(default=HashMap::new(), setter(skip))]
89 pub(crate) components_key_id: HashMap<String, u32>,
90 #[builder(default = 0, setter(skip))]
91 pub(crate) current_key: u32,
92
93 #[builder(via_mutators, default=Arc::new(AtomicBool::new(false)))]
94 pub(crate) encrypted_api: Arc<AtomicBool>,
95
96 #[builder(via_mutators)]
97 pub(crate) noise_psk: Vec<u8>,
98
99 #[builder(default=Arc::new(Mutex::new(None)), setter(skip))]
100 pub(crate) handshake_state:
101 Arc<Mutex<Option<HandshakeState<X25519, ChaCha20Poly1305, Sha256>>>>,
102 #[builder(default=Arc::new(Mutex::new(None)), setter(skip))]
103 pub(crate) encrypt_cypher: Arc<Mutex<Option<CipherState<ChaCha20Poly1305>>>>,
104 #[builder(default=Arc::new(Mutex::new(None)), setter(skip))]
105 pub(crate) decrypt_cypher: Arc<Mutex<Option<CipherState<ChaCha20Poly1305>>>>,
106
107 name: String,
108
109 #[builder(default = None, setter(strip_option))]
110 #[deprecated(note = "https://esphome.io/components/api.html#configuration-variables")]
111 password: Option<String>,
112 #[builder(default = None, setter(strip_option))]
113 encryption_key: Option<String>,
114
115 #[builder(default = 1)]
116 api_version_major: u32,
117 #[builder(default = 10)]
118 api_version_minor: u32,
119 #[builder(default="Rust: esphome-native-api".to_string())]
120 server_info: String,
121
122 #[builder(default = None, setter(strip_option))]
123 friendly_name: Option<String>,
124
125 #[builder(default = None, setter(strip_option))]
126 mac: Option<String>,
127
128 #[builder(default = None, setter(strip_option))]
129 model: Option<String>,
130
131 #[builder(default = None, setter(strip_option))]
132 manufacturer: Option<String>,
133 #[builder(default = None, setter(strip_option))]
134 suggested_area: Option<String>,
135 #[builder(default = None, setter(strip_option))]
136 bluetooth_mac_address: Option<String>,
137}
138
139/// Easier version of the API abstraction.
140///
141/// Manages entity keys internally.
142impl EspHomeServer {
143 /// Starts the ESPHome server and begins communication over the provided TCP stream.
144 ///
145 /// This method initializes the underlying [`EspHomeApi`], establishes the connection,
146 /// and spawns a background task to handle message routing between the API and
147 /// registered entities.
148 ///
149 /// # Arguments
150 ///
151 /// * `tcp_stream` - An established TCP connection to an ESPHome device
152 ///
153 /// # Returns
154 ///
155 /// Returns a [`Connection`] handle. Use [`Connection::sender`] to send
156 /// messages to the ESPHome device, [`Connection::receiver`] to receive
157 /// messages from it, and [`Connection::wait`] to observe when — and why —
158 /// the connection ends.
159 ///
160 /// # Errors
161 ///
162 /// Returns an error if the connection cannot be established or if the initial
163 /// handshake fails.
164 ///
165 /// # Examples
166 ///
167 /// ```rust,no_run
168 /// # use esphome_native_api::esphomeserver::EspHomeServer;
169 /// # use tokio::net::TcpStream;
170 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
171 /// let stream = TcpStream::connect("192.168.1.100:6053").await?;
172 /// let mut server = EspHomeServer::builder().name("client".to_string()).build();
173 /// let connection = server.start(stream).await?;
174 /// let tx = connection.sender();
175 /// let mut rx = connection.receiver();
176 /// # let _ = (tx, &mut rx);
177 /// # Ok(())
178 /// # }
179 /// ```
180 pub async fn start(&mut self, tcp_stream: TcpStream) -> Result<Connection, Error> {
181 let server = EspHomeApi::builder()
182 .api_version_major(self.api_version_major)
183 .api_version_minor(self.api_version_minor)
184 // .password(self.password.or_else())
185 .server_info(self.server_info.clone())
186 .name(self.name.clone())
187 // .friendly_name(self.friendly_name)
188 // .bluetooth_mac_address(self.bluetooth_mac_address)
189 // .mac(self.mac)
190 // .manufacturer(self.manufacturer)
191 // .model(self.model)
192 // .suggested_area(self.suggested_area)
193 .build()?;
194 let api_connection = server.start(tcp_stream).await?;
195 let messages_tx = api_connection.sender();
196 let mut messages_rx = api_connection.receiver();
197 let (outgoing_messages_tx, outgoing_messages_rx) = broadcast::channel::<ProtoMessage>(16);
198 let (done_tx, done_rx) = tokio::sync::oneshot::channel::<Result<(), Error>>();
199 let api_components_clone = self.components_by_key.clone();
200
201 tokio::spawn(async move {
202 loop {
203 let message = match messages_rx.recv().await {
204 Ok(message) => message,
205 // Lagging only skips messages; the connection is still alive.
206 Err(RecvError::Lagged(skipped)) => {
207 warn!("Message routing lagged, {skipped} messages skipped");
208 continue;
209 }
210 // The underlying connection closed; stop routing.
211 Err(RecvError::Closed) => break,
212 };
213 debug!("Received message: {:?}", message);
214
215 match message {
216 ProtoMessage::ListEntitiesRequest(list_entities_request) => {
217 debug!("ListEntitiesRequest: {:?}", list_entities_request);
218
219 for _sensor in api_components_clone.values() {
220 // TODO: Handle the different entity types
221 // let _ = outgoing_messages_tx.send(sensor.clone());
222 }
223 // No active receivers is fine (consumer dropped its handle).
224 let _ = outgoing_messages_tx.send(ProtoMessage::ListEntitiesDoneResponse(
225 ListEntitiesDoneResponse {},
226 ));
227 }
228 other_message => {
229 let _ = outgoing_messages_tx.send(other_message);
230 }
231 }
232 }
233
234 // Propagate the underlying connection's terminal outcome.
235 let _ = done_tx.send(api_connection.wait().await);
236 });
237
238 Ok(Connection::new(messages_tx, outgoing_messages_rx, done_rx))
239 }
240
241 /// Adds an entity to the server's internal registry.
242 ///
243 /// Each entity is assigned a unique key that is managed internally. The entity
244 /// can be referenced by its string identifier in subsequent operations.
245 ///
246 /// # Arguments
247 ///
248 /// * `entity_id` - A unique string identifier for the entity
249 /// * `entity` - The entity to register
250 ///
251 /// # Examples
252 ///
253 /// ```rust
254 /// # use esphome_native_api::esphomeserver::{EspHomeServer, Entity, BinarySensor};
255 /// let mut server = EspHomeServer::builder().name("server".to_string()).build();
256 /// let sensor = Entity::BinarySensor(BinarySensor {
257 /// object_id: "motion_sensor".to_string(),
258 /// });
259 /// server.add_entity("motion", sensor);
260 /// ```
261 pub fn add_entity(&mut self, entity_id: &str, entity: Entity) {
262 self.components_key_id
263 .insert(entity_id.to_string(), self.current_key);
264 self.components_by_key.insert(self.current_key, entity);
265
266 self.current_key += 1;
267 }
268}
269
270/// Represents different types of entities supported by ESPHome.
271///
272/// This enum contains all entity types that can be registered with the server.
273/// Currently, only binary sensors are implemented, but this will expand to include
274/// other entity types like switches, lights, sensors, etc.
275#[derive(Clone, Debug)]
276pub enum Entity {
277 /// A binary sensor entity (on/off state)
278 BinarySensor(BinarySensor),
279}
280
281/// Represents a binary sensor entity.
282///
283/// Binary sensors report a simple on/off or true/false state, such as
284/// door/window sensors, motion detectors, or binary switches.
285#[derive(Clone, Debug)]
286pub struct BinarySensor {
287 /// The unique object identifier for this binary sensor
288 pub object_id: String,
289}