diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 88618acef4..0e2c612279 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -226,32 +226,6 @@ def _encryption_schema(config): return ENCRYPTION_SCHEMA(config) -def _validate_api_config(config: ConfigType) -> ConfigType: - """Validate API configuration with mutual exclusivity check and deprecation warning.""" - # Check if both password and encryption are configured - has_password = CONF_PASSWORD in config and config[CONF_PASSWORD] - has_encryption = CONF_ENCRYPTION in config - - if has_password and has_encryption: - raise cv.Invalid( - "The 'password' and 'encryption' options are mutually exclusive. " - "The API client only supports one authentication method at a time. " - "Please remove one of them. " - "Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. " - "We strongly recommend using 'encryption' instead for better security." - ) - - # Warn about password deprecation - if has_password: - _LOGGER.warning( - "API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. " - "Please migrate to the 'encryption' configuration. " - "See https://esphome.io/components/api/#configuration-variables" - ) - - return config - - def _consume_api_sockets(config: ConfigType) -> ConfigType: """Register socket needs for API component.""" from esphome.components import socket @@ -268,7 +242,17 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(APIServer), cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + # Removed in 2026.1.0 - kept to provide helpful error message + cv.Optional(CONF_PASSWORD): cv.invalid( + "The 'password' option has been removed in ESPHome 2026.1.0.\n" + "Password authentication was deprecated in May 2022.\n" + "Please migrate to encryption for secure API communication:\n\n" + "api:\n" + " encryption:\n" + " key: !secret api_encryption_key\n\n" + "Generate a key with: openssl rand -base64 32\n" + "Or visit https://esphome.io/components/api/#configuration-variables" + ), cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" ): cv.positive_time_period_milliseconds, @@ -330,7 +314,6 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.rename_key(CONF_SERVICES, CONF_ACTIONS), - _validate_api_config, _consume_api_sockets, ) @@ -344,9 +327,6 @@ async def to_code(config: ConfigType) -> None: CORE.register_controller() cg.add(var.set_port(config[CONF_PORT])) - if config[CONF_PASSWORD]: - cg.add_define("USE_API_PASSWORD") - cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) if CONF_LISTEN_BACKLOG in config: diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 5efd839673..652b456850 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -7,10 +7,7 @@ service APIConnection { option (needs_setup_connection) = false; option (needs_authentication) = false; } - rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) { - option (needs_setup_connection) = false; - option (needs_authentication) = false; - } + // REMOVED in ESPHome 2026.1.0: rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { option (needs_setup_connection) = false; option (needs_authentication) = false; @@ -82,14 +79,13 @@ service APIConnection { // * VarInt denoting the type of message. // * The message object encoded as a ProtoBuf message -// The connection is established in 4 steps: +// The connection is established in 2 steps: // * First, the client connects to the server and sends a "Hello Request" identifying itself -// * The server responds with a "Hello Response" and selects the protocol version -// * After receiving this message, the client attempts to authenticate itself using -// the password and a "Connect Request" -// * The server responds with a "Connect Response" and notifies of invalid password. +// * The server responds with a "Hello Response" and the connection is authenticated // If anything in this initial process fails, the connection must immediately closed // by both sides and _no_ disconnection message is to be sent. +// Note: Password authentication via AuthenticationRequest/AuthenticationResponse (message IDs 3, 4) +// was removed in ESPHome 2026.1.0. Those message IDs are reserved and should not be reused. // Message sent at the beginning of each connection // Can only be sent by the client and only at the beginning of the connection @@ -130,25 +126,23 @@ message HelloResponse { string name = 4; } -// Message sent at the beginning of each connection to authenticate the client -// Can only be sent by the client and only at the beginning of the connection +// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported. +// These messages are kept for protocol documentation but are not processed by the server. +// Use noise encryption instead: https://esphome.io/components/api/#configuration-variables message AuthenticationRequest { option (id) = 3; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_PASSWORD"; + option deprecated = true; - // The password to log in with string password = 1; } -// Confirmation of successful connection. After this the connection is available for all traffic. -// Can only be sent by the server and only at the beginning of the connection message AuthenticationResponse { option (id) = 4; option (source) = SOURCE_SERVER; option (no_delay) = true; - option (ifdef) = "USE_API_PASSWORD"; + option deprecated = true; bool invalid_password = 1; } @@ -205,7 +199,9 @@ message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; - bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"]; + // Deprecated in ESPHome 2026.1.0, but kept for backward compatibility + // with older ESPHome versions that still send this field. + bool uses_password = 1 [deprecated = true]; // The name of the node, given by "App.set_name()" string name = 2; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2ecd54bb00..3ded5e4408 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1535,27 +1535,11 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { resp.set_server_info(ESPHOME_VERSION_REF); resp.set_name(StringRef(App.get_name())); -#ifdef USE_API_PASSWORD - // Password required - wait for authentication - this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); -#else - // No password configured - auto-authenticate + // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); -#endif return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -#ifdef USE_API_PASSWORD -bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) { - AuthenticationResponse resp; - // bool invalid_password = 1; - resp.invalid_password = !this->parent_->check_password(msg.password.byte(), msg.password.size()); - if (!resp.invalid_password) { - this->complete_authentication_(); - } - return this->send_message(resp, AuthenticationResponse::MESSAGE_TYPE); -} -#endif // USE_API_PASSWORD bool APIConnection::send_ping_response(const PingRequest &msg) { PingResponse resp; @@ -1564,9 +1548,6 @@ bool APIConnection::send_ping_response(const PingRequest &msg) { bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; -#ifdef USE_API_PASSWORD - resp.uses_password = true; -#endif resp.set_name(StringRef(App.get_name())); resp.set_friendly_name(StringRef(App.get_friendly_name())); #ifdef USE_AREAS @@ -1845,12 +1826,6 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { // Do not set last_traffic_ on send return true; } -#ifdef USE_API_PASSWORD -void APIConnection::on_unauthenticated_access() { - this->on_fatal_error(); - ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); -} -#endif void APIConnection::on_no_setup_connection() { this->on_fatal_error(); ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 59c42aa033..ffe3614f20 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -203,9 +203,6 @@ class APIConnection final : public APIServerConnection { void on_get_time_response(const GetTimeResponse &value) override; #endif bool send_hello_response(const HelloRequest &msg) override; -#ifdef USE_API_PASSWORD - bool send_authenticate_response(const AuthenticationRequest &msg) override; -#endif bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_ping_response(const PingRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override; @@ -261,9 +258,6 @@ class APIConnection final : public APIServerConnection { } void on_fatal_error() override; -#ifdef USE_API_PASSWORD - void on_unauthenticated_access() override; -#endif void on_no_setup_connection() override; ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { // FIXME: ensure no recursive writes can happen diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 698e08f9b3..d26b309552 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -43,21 +43,6 @@ void HelloResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->server_info_ref_.size()); size.add_length(1, this->name_ref_.size()); } -#ifdef USE_API_PASSWORD -bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->password = StringRef(reinterpret_cast(value.data()), value.size()); - break; - } - default: - return false; - } - return true; -} -void AuthenticationResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } -void AuthenticationResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); } -#endif #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); @@ -81,9 +66,6 @@ void DeviceInfo::calculate_size(ProtoSize &size) const { } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { -#ifdef USE_API_PASSWORD - buffer.encode_bool(1, this->uses_password); -#endif buffer.encode_string(2, this->name_ref_); buffer.encode_string(3, this->mac_address_ref_); buffer.encode_string(4, this->esphome_version_ref_); @@ -139,9 +121,6 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #endif } void DeviceInfoResponse::calculate_size(ProtoSize &size) const { -#ifdef USE_API_PASSWORD - size.add_bool(1, this->uses_password); -#endif size.add_length(1, this->name_ref_.size()); size.add_length(1, this->mac_address_ref_.size()); size.add_length(1, this->esphome_version_ref_.size()); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e08cf65bb9..0605051e29 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -393,39 +393,6 @@ class HelloResponse final : public ProtoMessage { protected: }; -#ifdef USE_API_PASSWORD -class AuthenticationRequest final : public ProtoDecodableMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 3; - static constexpr uint8_t ESTIMATED_SIZE = 9; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "authentication_request"; } -#endif - StringRef password{}; -#ifdef HAS_PROTO_MESSAGE_DUMP - void dump_to(std::string &out) const override; -#endif - - protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; -}; -class AuthenticationResponse final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 4; - static constexpr uint8_t ESTIMATED_SIZE = 2; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "authentication_response"; } -#endif - bool invalid_password{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - void dump_to(std::string &out) const override; -#endif - - protected: -}; -#endif class DisconnectRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 5; @@ -525,12 +492,9 @@ class DeviceInfo final : public ProtoMessage { class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 257; + static constexpr uint8_t ESTIMATED_SIZE = 255; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } -#endif -#ifdef USE_API_PASSWORD - bool uses_password{false}; #endif StringRef name_ref_{}; void set_name(const StringRef &ref) { this->name_ref_ = ref; } diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 1ec6645b3f..ac5f04f3fb 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -748,18 +748,6 @@ void HelloResponse::dump_to(std::string &out) const { dump_field(out, "server_info", this->server_info_ref_); dump_field(out, "name", this->name_ref_); } -#ifdef USE_API_PASSWORD -void AuthenticationRequest::dump_to(std::string &out) const { - MessageDumpHelper helper(out, "AuthenticationRequest"); - out.append(" password: "); - out.append("'").append(this->password.c_str(), this->password.size()).append("'"); - out.append("\n"); -} -void AuthenticationResponse::dump_to(std::string &out) const { - MessageDumpHelper helper(out, "AuthenticationResponse"); - dump_field(out, "invalid_password", this->invalid_password); -} -#endif void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } @@ -782,9 +770,6 @@ void DeviceInfo::dump_to(std::string &out) const { #endif void DeviceInfoResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "DeviceInfoResponse"); -#ifdef USE_API_PASSWORD - dump_field(out, "uses_password", this->uses_password); -#endif dump_field(out, "name", this->name_ref_); dump_field(out, "mac_address", this->mac_address_ref_); dump_field(out, "esphome_version", this->esphome_version_ref_); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 984cb0bb6e..c9bf638ad7 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -24,17 +24,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_hello_request(msg); break; } -#ifdef USE_API_PASSWORD - case AuthenticationRequest::MESSAGE_TYPE: { - AuthenticationRequest msg; - msg.decode(msg_data, msg_size); -#ifdef HAS_PROTO_MESSAGE_DUMP - ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str()); -#endif - this->on_authentication_request(msg); - break; - } -#endif case DisconnectRequest::MESSAGE_TYPE: { DisconnectRequest msg; // Empty message: no decode needed @@ -643,13 +632,6 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) { this->on_fatal_error(); } } -#ifdef USE_API_PASSWORD -void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) { - if (!this->send_authenticate_response(msg)) { - this->on_fatal_error(); - } -} -#endif void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { if (!this->send_disconnect_response(msg)) { this->on_fatal_error(); @@ -841,10 +823,7 @@ void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages switch (msg_type) { - case HelloRequest::MESSAGE_TYPE: // No setup required -#ifdef USE_API_PASSWORD - case AuthenticationRequest::MESSAGE_TYPE: // No setup required -#endif + case HelloRequest::MESSAGE_TYPE: // No setup required case DisconnectRequest::MESSAGE_TYPE: // No setup required case PingRequest::MESSAGE_TYPE: // No setup required break; // Skip all checks for these messages diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 261d9fbd27..e2a23827dc 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -26,10 +26,6 @@ class APIServerConnectionBase : public ProtoService { virtual void on_hello_request(const HelloRequest &value){}; -#ifdef USE_API_PASSWORD - virtual void on_authentication_request(const AuthenticationRequest &value){}; -#endif - virtual void on_disconnect_request(const DisconnectRequest &value){}; virtual void on_disconnect_response(const DisconnectResponse &value){}; virtual void on_ping_request(const PingRequest &value){}; @@ -228,9 +224,6 @@ class APIServerConnectionBase : public ProtoService { class APIServerConnection : public APIServerConnectionBase { public: virtual bool send_hello_response(const HelloRequest &msg) = 0; -#ifdef USE_API_PASSWORD - virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0; -#endif virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; virtual bool send_ping_response(const PingRequest &msg) = 0; virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; @@ -357,9 +350,6 @@ class APIServerConnection : public APIServerConnectionBase { #endif protected: void on_hello_request(const HelloRequest &msg) override; -#ifdef USE_API_PASSWORD - void on_authentication_request(const AuthenticationRequest &msg) override; -#endif void on_disconnect_request(const DisconnectRequest &msg) override; void on_ping_request(const PingRequest &msg) override; void on_device_info_request(const DeviceInfoRequest &msg) override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 8b76740e4d..eedf8c7172 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -224,38 +224,6 @@ void APIServer::dump_config() { #endif } -#ifdef USE_API_PASSWORD -bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const { - // depend only on input password length - const char *a = this->password_.c_str(); - uint32_t len_a = this->password_.length(); - const char *b = reinterpret_cast(password_data); - uint32_t len_b = password_len; - - // disable optimization with volatile - volatile uint32_t length = len_b; - volatile const char *left = nullptr; - volatile const char *right = b; - uint8_t result = 0; - - if (len_a == length) { - left = *((volatile const char **) &a); - result = 0; - } - if (len_a != length) { - left = b; - result = 1; - } - - for (size_t i = 0; i < length; i++) { - result |= *left++ ^ *right++; // NOLINT - } - - return result == 0; -} - -#endif - void APIServer::handle_disconnect(APIConnection *conn) {} // Macro for controller update dispatch @@ -377,10 +345,6 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; void APIServer::set_port(uint16_t port) { this->port_ = port; } -#ifdef USE_API_PASSWORD -void APIServer::set_password(const std::string &password) { this->password_ = password; } -#endif - void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } #ifdef USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index b855d2cce0..2b2e8bae73 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -59,10 +59,6 @@ class APIServer : public Component, #endif #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; -#endif -#ifdef USE_API_PASSWORD - bool check_password(const uint8_t *password_data, size_t password_len) const; - void set_password(const std::string &password); #endif void set_port(uint16_t port); void set_reboot_timeout(uint32_t reboot_timeout); @@ -256,9 +252,6 @@ class APIServer : public Component, // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; -#ifdef USE_API_PASSWORD - std::string password_; -#endif std::vector shared_write_buffer_; // Shared proto write buffer for all connections #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index efdab9341c..f8b5cd9a5d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -833,9 +833,6 @@ class ProtoService { virtual bool is_authenticated() = 0; virtual bool is_connection_setup() = 0; virtual void on_fatal_error() = 0; -#ifdef USE_API_PASSWORD - virtual void on_unauthenticated_access() = 0; -#endif virtual void on_no_setup_connection() = 0; /** * Create a buffer with a reserved size. @@ -873,20 +870,7 @@ class ProtoService { return true; } - inline bool check_authenticated_() { -#ifdef USE_API_PASSWORD - if (!this->check_connection_setup_()) { - return false; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return false; - } - return true; -#else - return this->check_connection_setup_(); -#endif - } + inline bool check_authenticated_() { return this->check_connection_setup_(); } }; } // namespace esphome::api diff --git a/tests/integration/fixtures/host_mode_api_password.yaml b/tests/integration/fixtures/host_mode_api_password.yaml deleted file mode 100644 index 038b6871e0..0000000000 --- a/tests/integration/fixtures/host_mode_api_password.yaml +++ /dev/null @@ -1,14 +0,0 @@ -esphome: - name: host-mode-api-password -host: -api: - password: "test_password_123" -logger: - level: DEBUG -# Test sensor to verify connection works -sensor: - - platform: template - name: Test Sensor - id: test_sensor - lambda: return 42.0; - update_interval: 0.1s diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py deleted file mode 100644 index 5c5e689e45..0000000000 --- a/tests/integration/test_host_mode_api_password.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Integration test for API password authentication.""" - -from __future__ import annotations - -import asyncio - -from aioesphomeapi import APIConnectionError, InvalidAuthAPIError -import pytest - -from .types import APIClientConnectedFactory, RunCompiledFunction - - -@pytest.mark.asyncio -async def test_host_mode_api_password( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """Test API authentication with password.""" - async with run_compiled(yaml_config): - # Connect with correct password - async with api_client_connected(password="test_password_123") as client: - # Verify we can get device info - device_info = await client.device_info() - assert device_info is not None - assert device_info.uses_password is True - assert device_info.name == "host-mode-api-password" - - # Subscribe to states to ensure authenticated connection works - loop = asyncio.get_running_loop() - state_future: asyncio.Future[bool] = loop.create_future() - states = {} - - def on_state(state): - states[state.key] = state - if not state_future.done(): - state_future.set_result(True) - - client.subscribe_states(on_state) - - # Wait for at least one state with timeout - try: - await asyncio.wait_for(state_future, timeout=5.0) - except TimeoutError: - pytest.fail("No states received within timeout") - - # Should have received at least one state (the test sensor) - assert len(states) > 0 - - # Test with wrong password - should fail - # Try connecting with wrong password - try: - async with api_client_connected( - password="wrong_password", timeout=5 - ) as client: - # If we get here without exception, try to use the connection - # which should fail if auth failed - await client.device_info_and_list_entities() - # If we successfully got device info and entities, auth didn't fail properly - pytest.fail("Connection succeeded with wrong password") - except (InvalidAuthAPIError, APIConnectionError) as e: - # Expected - auth should fail - # Accept either InvalidAuthAPIError or generic APIConnectionError - # since the client might not always distinguish - assert ( - "password" in str(e).lower() - or "auth" in str(e).lower() - or "invalid" in str(e).lower() - )