Skip to main content

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}