Skip to main content

esphome_native_api/
esphomeapi.rs

1//! Low-level ESPHome native API implementation.
2//!
3//! This module provides [`EspHomeApi`], which handles the core protocol communication
4//! with ESPHome devices. It manages connection establishment, encryption handshakes,
5//! message framing, and protocol state.
6//!
7//! # Examples
8//!
9//! ## Plaintext Connection
10//!
11//! ```rust,no_run
12//! use esphome_native_api::esphomeapi::EspHomeApi;
13//! use tokio::net::TcpStream;
14//!
15//! #[tokio::main]
16//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
17//!     let stream = TcpStream::connect("192.168.1.100:6053").await?;
18//!     
19//!     let mut api = EspHomeApi::builder()
20//!         .name("my-client".to_string())
21//!         .build();
22//!     
23//!     let (tx, mut rx) = api.start(stream).await?;
24//!     Ok(())
25//! }
26//! ```
27//!
28//! ## Encrypted Connection
29//!
30//! ```rust,no_run
31//! use esphome_native_api::esphomeapi::EspHomeApi;
32//! use tokio::net::TcpStream;
33//!
34//! #[tokio::main]
35//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
36//!     let stream = TcpStream::connect("192.168.1.100:6053").await?;
37//!     
38//!     let mut api = EspHomeApi::builder()
39//!         .name("my-client".to_string())
40//!         .encryption_key("your-base64-encoded-key".to_string())
41//!         .build();
42//!     
43//!     let (tx, mut rx) = api.start(stream).await?;
44//!     Ok(())
45//! }
46//! ```
47
48use base64::prelude::*;
49use futures::sink::SinkExt;
50use log::debug;
51use log::error;
52use log::info;
53use log::trace;
54use noise_protocol::CipherState;
55use noise_protocol::ErrorKind;
56use noise_protocol::HandshakeState;
57use noise_protocol::patterns::noise_nn_psk0;
58use noise_rust_crypto::ChaCha20Poly1305;
59use noise_rust_crypto::Sha256;
60use noise_rust_crypto::X25519;
61use std::sync::Arc;
62use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
63use tokio::sync::Mutex;
64use tokio::sync::broadcast;
65use tokio::sync::mpsc;
66use tokio::sync::oneshot;
67use tokio_stream::StreamExt;
68use tokio_util::codec::FramedRead;
69use tokio_util::codec::FramedWrite;
70use typed_builder::TypedBuilder;
71
72use crate::frame::FrameCodec;
73use crate::packet_encrypted;
74use crate::packet_plaintext;
75use crate::parser::ProtoMessage;
76use crate::proto::{
77    self, AuthenticationResponse, DeviceInfoResponse, DisconnectResponse, HelloResponse,
78    PingResponse,
79};
80
81async fn write_error_and_disconnect<W>(mut writer: FramedWrite<W, FrameCodec>, message: &str)
82where
83    W: AsyncWrite + Unpin,
84{
85    error!("API Failure: {}. Disconnecting.", message);
86    let packet = [[1].to_vec(), message.as_bytes().to_vec()].concat();
87    writer.send(packet).await.unwrap();
88    writer.flush().await.unwrap();
89    let mut tcp_write = writer.into_inner();
90    if let Err(err) = tcp_write.shutdown().await {
91        error!("failed to shutdown socket: {:?}", err);
92    }
93}
94
95const ERROR_ONLY_ENCRYPTED: &str = "Only key encryption is enabled";
96const ERROR_HANDSHAKE_MAC_FAILURE: &str = "Handshake MAC failure";
97
98/// Low-level ESPHome native API client.
99///
100/// `EspHomeApi` provides direct access to the ESPHome native API protocol,
101/// handling connection setup, encryption, and message framing. This is the
102/// lower-level API that [`crate::esphomeserver::EspHomeServer`] builds upon.
103///
104/// This struct supports both encrypted and plaintext connections and uses
105/// the builder pattern for configuration via [`TypedBuilder`].
106///
107/// # Builder Options
108///
109/// - `name`: Device name (required)
110/// - `encryption_key`: Base64-encoded encryption key (optional, enables encryption)
111/// - `api_version_major`: API version major number (default: 1)
112/// - `api_version_minor`: API version minor number (default: 10)
113/// - `server_info`: Server identification string (default: "Rust: esphome-native-api")
114/// - `friendly_name`: Human-readable device name (optional)
115/// - `mac`: MAC address (optional)
116/// - `model`: Device model (optional)
117/// - `manufacturer`: Device manufacturer (optional)
118/// - `suggested_area`: Suggested area for the device (optional)
119/// - `bluetooth_mac_address`: Bluetooth MAC address (optional)
120///
121/// # Examples
122///
123/// ```rust
124/// use esphome_native_api::esphomeapi::EspHomeApi;
125///
126/// let api = EspHomeApi::builder()
127///     .name("bedroom-light".to_string())
128///     .api_version_major(1)
129///     .api_version_minor(10)
130///     .friendly_name("Bedroom Light".to_string())
131///     .build();
132/// ```
133#[derive(TypedBuilder, Clone)]
134pub struct EspHomeApi {
135    // Private fields
136    name: String,
137
138    #[builder(default = None, setter(strip_option(fallback=encryption_key_opt)))]
139    encryption_key: Option<String>,
140
141    #[builder(default = 1)]
142    api_version_major: u32,
143    #[builder(default = 10)]
144    api_version_minor: u32,
145    #[builder(default="Rust: esphome-native-api".to_string())]
146    server_info: String,
147
148    #[builder(default = None, setter(strip_option(fallback=friendly_name_opt)))]
149    friendly_name: Option<String>,
150
151    #[builder(default = None, setter(strip_option(fallback=mac_opt)))]
152    mac: Option<String>,
153
154    #[builder(default = None, setter(strip_option(fallback=model_opt)))]
155    model: Option<String>,
156
157    #[builder(default = None, setter(strip_option(fallback=manufacturer_opt)))]
158    manufacturer: Option<String>,
159    #[builder(default = None, setter(strip_option(fallback=suggested_area_opt)))]
160    suggested_area: Option<String>,
161    #[builder(default = None, setter(strip_option(fallback=bluetooth_mac_address_opt)))]
162    bluetooth_mac_address: Option<String>,
163
164    #[builder(default = None, setter(strip_option(fallback=project_name_opt)))]
165    project_name: Option<String>,
166
167    #[builder(default = None, setter(strip_option(fallback=project_version_opt)))]
168    project_version: Option<String>,
169    #[builder(default = None, setter(strip_option(fallback=compilation_time_opt)))]
170    compilation_time: Option<String>,
171
172    #[builder(default = 0)]
173    legacy_bluetooth_proxy_version: u32,
174    #[builder(default = 0)]
175    bluetooth_proxy_feature_flags: u32,
176    #[builder(default = 0)]
177    legacy_voice_assistant_version: u32,
178    #[builder(default = 0)]
179    voice_assistant_feature_flags: u32,
180}
181
182/// Handles the ESPHome API protocol with encryption support.
183impl EspHomeApi {
184    /// Starts the API client and establishes communication with an ESPHome device.
185    ///
186    /// This method performs the complete connection handshake, including:
187    /// 1. Detecting whether encryption is required
188    /// 2. Performing encryption handshake if needed
189    /// 3. Exchanging hello messages
190    /// 4. Setting up message routing
191    ///
192    /// # Arguments
193    ///
194    /// * `tcp_stream` - An established TCP connection to the ESPHome device
195    ///
196    /// # Returns
197    ///
198    /// Returns a tuple containing:
199    /// - An `mpsc::Sender` for sending messages to the device
200    /// - A `broadcast::Receiver` for receiving messages from the device
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if:
205    /// - The connection fails
206    /// - The encryption handshake fails
207    /// - The hello exchange fails
208    /// - The device requires encryption but no key was provided
209    ///
210    /// # Examples
211    ///
212    /// ```rust,no_run
213    /// # use esphome_native_api::esphomeapi::EspHomeApi;
214    /// # use tokio::net::TcpStream;
215    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
216    /// let stream = TcpStream::connect("192.168.1.100:6053").await?;
217    /// let mut api = EspHomeApi::builder().name("client".to_string()).build();
218    /// let (tx, mut rx) = api.start(stream).await?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    pub async fn start<S>(
223        &self,
224        stream: S,
225    ) -> Result<
226        (
227            mpsc::Sender<ProtoMessage>,
228            broadcast::Receiver<ProtoMessage>,
229        ),
230        Box<dyn std::error::Error>,
231    >
232    where
233        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
234    {
235        // Channel for messages
236        let (answer_messages_tx, mut answer_messages_rx) = mpsc::channel::<ProtoMessage>(16);
237        let (outgoing_messages_tx, outgoing_messages_rx) = broadcast::channel::<ProtoMessage>(16);
238
239        #[allow(deprecated)]
240        let device_info = DeviceInfoResponse {
241            api_encryption_supported: self.encryption_key.is_some(),
242            uses_password: false,
243            name: self.name.clone(),
244            mac_address: self.mac.clone().unwrap_or_default(),
245            esphome_version: proto::VERSION.to_owned(),
246            compilation_time: self.compilation_time.clone().unwrap_or_default(),
247            model: self.model.clone().unwrap_or_default(),
248            has_deep_sleep: false,
249            project_name: self.project_name.clone().unwrap_or_default(),
250            project_version: self.project_version.clone().unwrap_or_default(),
251            webserver_port: 0,
252            // See https://github.com/esphome/aioesphomeapi/blob/c1fee2f4eaff84d13ca71996bb272c28b82314fc/aioesphomeapi/model.py#L154
253            legacy_bluetooth_proxy_version: self.legacy_bluetooth_proxy_version,
254            bluetooth_proxy_feature_flags: self.bluetooth_proxy_feature_flags,
255            manufacturer: self.manufacturer.clone().unwrap_or_default(),
256            friendly_name: self.friendly_name.clone().unwrap_or(self.name.clone()),
257            legacy_voice_assistant_version: self.legacy_voice_assistant_version,
258            voice_assistant_feature_flags: self.voice_assistant_feature_flags,
259            suggested_area: self.suggested_area.clone().unwrap_or_default(),
260            bluetooth_mac_address: self.bluetooth_mac_address.clone().unwrap_or_default(),
261            areas: vec![],
262            devices: vec![],
263            area: None,
264            zwave_proxy_feature_flags: 0,
265            zwave_home_id: 0,
266            serial_proxies: vec![],
267        };
268
269        let hello_response = HelloResponse {
270            api_version_major: self.api_version_major,
271            api_version_minor: self.api_version_minor,
272            server_info: self.server_info.clone(),
273            name: self.name.clone(),
274        };
275
276        // Per-connection cipher state — created fresh so multiple concurrent connections
277        // never share encryption context.
278        let encrypt_cypher: Arc<Mutex<Option<CipherState<ChaCha20Poly1305>>>> =
279            Arc::new(Mutex::new(None));
280        let decrypt_cypher: Arc<Mutex<Option<CipherState<ChaCha20Poly1305>>>> =
281            Arc::new(Mutex::new(None));
282
283        // Stage 1: Initialization
284        trace!("Init Connection: Stage 1");
285        let encryption_key = self.encryption_key.clone();
286
287        let (stream_read, stream_write) = tokio::io::split(stream);
288        let mut stream_read = BufReader::new(stream_read);
289
290        let peeked_bytes = stream_read.fill_buf().await?;
291        if peeked_bytes.is_empty() {
292            return Err("No data".into());
293        }
294
295        trace!("TCP Peeked: {:02X?}", &peeked_bytes[0..1]);
296
297        let preamble = peeked_bytes[0] as usize;
298
299        let plaintext_communication = match preamble {
300            0 => {
301                debug!("Cleartext messaging");
302                true
303            }
304            1 => {
305                trace!("Encrypted messaging");
306                false
307            }
308            _ => {
309                return Err(format!("Invalid marker byte {}", preamble).into());
310            }
311        };
312        let encrypted = !plaintext_communication;
313
314        let decoder = FrameCodec::new(encrypted);
315        let encoder = FrameCodec::new(encrypted);
316        let mut reader = FramedRead::new(stream_read, decoder);
317        let mut writer = FramedWrite::new(stream_write, encoder);
318
319        if plaintext_communication {
320            if self.encryption_key.is_some() {
321                let encoder = FrameCodec::new(true);
322                let writer = FramedWrite::new(writer.into_inner(), encoder);
323                write_error_and_disconnect(writer, ERROR_ONLY_ENCRYPTED).await;
324                return Err(ERROR_ONLY_ENCRYPTED.into());
325            }
326        } else {
327            if self.encryption_key.is_none() {
328                write_error_and_disconnect(writer, "No encrypted communication allowed").await;
329                return Err("No encryption key set, but encrypted communication requested.".into());
330            }
331
332            let frame_noise_hello = reader.next().await.unwrap().unwrap();
333            debug!("Frame 1: {:02X?}", &frame_noise_hello);
334
335            let message_server_hello =
336                packet_encrypted::generate_server_hello_frame(self.name.clone(), self.mac.clone());
337
338            writer.send(message_server_hello.clone()).await.unwrap();
339            writer.flush().await.unwrap();
340
341            let frame_handshake_request = reader.next().await.unwrap().unwrap();
342            debug!("Frame 2: {:02X?}", &frame_handshake_request);
343
344            // Similar to https://github.com/esphome/aioesphomeapi/blob/60bcd1698dd622aeac6f4b5ec448bab0e3467c4f/aioesphomeapi/_frame_helper/noise.py#L248C17-L255
345            let mut handshake_state: HandshakeState<X25519, ChaCha20Poly1305, Sha256> =
346                HandshakeState::new(
347                    noise_nn_psk0(),
348                    false,
349                    // NEXT: This is somehow set from the first api message?
350                    b"NoiseAPIInit\0\0",
351                    None,
352                    None,
353                    None,
354                    None,
355                );
356
357            let noise_psk = BASE64_STANDARD
358                .decode(encryption_key.as_ref().unwrap())
359                .unwrap();
360
361            handshake_state.push_psk(&noise_psk);
362            // Ignore message type byte
363            match handshake_state.read_message_vec(&frame_handshake_request[1..]) {
364                Ok(_) => {}
365                Err(e) => match e.kind() {
366                    ErrorKind::Decryption => {
367                        write_error_and_disconnect(writer, ERROR_HANDSHAKE_MAC_FAILURE).await;
368                        return Err(ERROR_HANDSHAKE_MAC_FAILURE.into());
369                    }
370                    _ => {
371                        debug!("Failed to read message: {}", e);
372                    }
373                },
374            }
375
376            let out = handshake_state.write_message_vec(b"").unwrap();
377            {
378                let mut encrypt_cipher_changer = encrypt_cypher.lock().await;
379                let mut decrypt_cipher_changer = decrypt_cypher.lock().await;
380                let (decrypt_cipher, encrypt_cipher) = handshake_state.get_ciphers();
381                *encrypt_cipher_changer = Some(encrypt_cipher);
382                *decrypt_cipher_changer = Some(decrypt_cipher);
383            }
384
385            let mut message_handshake = vec![0];
386            message_handshake.extend(out);
387
388            debug!("Sending handshake");
389            writer.send(message_handshake.clone()).await.unwrap();
390            writer.flush().await.unwrap();
391        }
392
393        debug!("Initialization done.");
394
395        // Asynchronously wait for an inbound socket.
396        let (cancellation_write_tx, mut cancellation_write_rx) = oneshot::channel();
397
398        // Write Loop
399        let encrypt_cypher_for_write = encrypt_cypher;
400        tokio::spawn(async move {
401            loop {
402                let answer_message: ProtoMessage;
403
404                // Wait for any new message
405                tokio::select! {
406                    biased; // Poll cancellation_write_rx first
407                    cancel_message = &mut cancellation_write_rx => {
408                        debug!("Write loop received cancellation signal ({}), exiting.", cancel_message.unwrap());
409                        break;
410                    }
411                    message = answer_messages_rx.recv() => {
412                        answer_message = message.unwrap();
413                    }
414                };
415
416                debug!("Answer message: {:?}", answer_message);
417
418                if plaintext_communication {
419                    writer
420                        .send(packet_plaintext::message_to_packet(&answer_message).unwrap())
421                        .await
422                        .unwrap();
423                    // answer_buf =
424                    //     [answer_buf, to_unencrypted_frame(&answer_message).unwrap()].concat();
425                } else {
426                    // Use normal messaging
427                    let mut encrypt_cipher_changer = encrypt_cypher_for_write.lock().await;
428                    writer
429                        .send(
430                            packet_encrypted::message_to_packet(
431                                &answer_message,
432                                &mut *encrypt_cipher_changer.as_mut().unwrap(),
433                            )
434                            .unwrap(),
435                        )
436                        .await
437                        .unwrap();
438                }
439                writer.flush().await.unwrap();
440
441                if matches!(answer_message, ProtoMessage::DisconnectResponse(_)) {
442                    debug!("Disconnecting");
443                    let mut tcp_write = writer.into_inner();
444                    match tcp_write.shutdown().await {
445                        Err(err) => {
446                            error!("failed to shutdown socket: {:?}", err);
447                            break;
448                        }
449                        _ => break,
450                    }
451                }
452            }
453        });
454
455        // Clone all necessary data before spawning the task
456        let answer_messages_tx_clone = answer_messages_tx.clone();
457        // Read Loop
458        tokio::spawn(async move {
459            loop {
460                let next = reader.next().await;
461                if next.is_none() {
462                    info!("Read loop stopped because stream finished");
463                    // If sending fails, the write loop is probably already closed
464                    let _ = cancellation_write_tx.send("read loop finished");
465                    break;
466                }
467                let frame = next.unwrap().unwrap();
468                trace!("TCP Receive: {:02X?}", &frame);
469
470                let message;
471                if encrypted {
472                    let mut decrypt_cipher_changer = decrypt_cypher.lock().await;
473                    message = packet_encrypted::packet_to_message(
474                        &frame,
475                        &mut *decrypt_cipher_changer.as_mut().unwrap(),
476                    )
477                    .unwrap();
478                } else {
479                    message = packet_plaintext::packet_to_message(&frame).unwrap();
480                }
481
482                // Authenticated Messages
483                match &message {
484                    ProtoMessage::DisconnectRequest(disconnect_request) => {
485                        debug!("DisconnectRequest: {:?}", disconnect_request);
486                        let response_message = DisconnectResponse {};
487                        answer_messages_tx_clone
488                            .send(ProtoMessage::DisconnectResponse(response_message))
489                            .await
490                            .unwrap();
491                        continue;
492                    }
493                    ProtoMessage::PingRequest(ping_request) => {
494                        debug!("PingRequest: {:?}", ping_request);
495                        let response_message = PingResponse {};
496                        answer_messages_tx_clone
497                            .send(ProtoMessage::PingResponse(response_message))
498                            .await
499                            .unwrap();
500                    }
501                    ProtoMessage::DeviceInfoRequest(device_info_request) => {
502                        debug!("DeviceInfoRequest: {:?}", device_info_request);
503                        answer_messages_tx_clone
504                            .send(ProtoMessage::DeviceInfoResponse(device_info.clone()))
505                            .await
506                            .unwrap();
507                    }
508                    ProtoMessage::HelloRequest(hello_request) => {
509                        debug!("HelloRequest: {:?}", hello_request);
510
511                        answer_messages_tx_clone
512                            .send(ProtoMessage::HelloResponse(hello_response.clone()))
513                            .await
514                            .unwrap();
515                    }
516                    ProtoMessage::AuthenticationRequest(authentication_request) => {
517                        debug!("AuthenticationRequest: {:?}", authentication_request);
518
519                        if authentication_request.password != "" {
520                            info!("Password Authentication is not supported");
521                        } else {
522                            let response_message = AuthenticationResponse {
523                                invalid_password: false,
524                            };
525                            answer_messages_tx_clone
526                                .send(ProtoMessage::AuthenticationResponse(response_message))
527                                .await
528                                .unwrap();
529                        }
530                    }
531                    message => {
532                        outgoing_messages_tx.send(message.clone()).unwrap();
533                    }
534                }
535            }
536        });
537
538        Ok((answer_messages_tx.clone(), outgoing_messages_rx))
539    }
540}