mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'template_send_message' into memory_api
This commit is contained in:
		| @@ -5,6 +5,7 @@ from esphome.components.esp32.const import ( | |||||||
|     VARIANT_ESP32, |     VARIANT_ESP32, | ||||||
|     VARIANT_ESP32C2, |     VARIANT_ESP32C2, | ||||||
|     VARIANT_ESP32C3, |     VARIANT_ESP32C3, | ||||||
|  |     VARIANT_ESP32C5, | ||||||
|     VARIANT_ESP32C6, |     VARIANT_ESP32C6, | ||||||
|     VARIANT_ESP32H2, |     VARIANT_ESP32H2, | ||||||
|     VARIANT_ESP32S2, |     VARIANT_ESP32S2, | ||||||
| @@ -85,6 +86,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | |||||||
|         3: adc_channel_t.ADC_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc_channel_t.ADC_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|  |     # ESP32-C5 ADC1 pin mapping - based on official ESP-IDF documentation | ||||||
|  |     # https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/api-reference/peripherals/gpio.html | ||||||
|  |     VARIANT_ESP32C5: { | ||||||
|  |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|  |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|  |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|  |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|  |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|  |         6: adc_channel_t.ADC_CHANNEL_5, | ||||||
|  |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C6: { |     VARIANT_ESP32C6: { | ||||||
|         0: adc_channel_t.ADC_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
| @@ -155,6 +166,8 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | |||||||
|     VARIANT_ESP32C3: { |     VARIANT_ESP32C3: { | ||||||
|         5: adc_channel_t.ADC_CHANNEL_0, |         5: adc_channel_t.ADC_CHANNEL_0, | ||||||
|     }, |     }, | ||||||
|  |     # ESP32-C5 has no ADC2 channels | ||||||
|  |     VARIANT_ESP32C5: {},  # no ADC2 | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C6: {},  # no ADC2 |     VARIANT_ESP32C6: {},  # no ADC2 | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||||
|   | |||||||
| @@ -104,9 +104,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | |||||||
|   /// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11). |   /// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11). | ||||||
|   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } |   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||||
|  |  | ||||||
|   /// Configure the ADC to use a specific channel on ADC1. |   /// Configure the ADC to use a specific channel on a specific ADC unit. | ||||||
|   /// This sets the channel for single-shot or continuous ADC measurements. |   /// This sets the channel for single-shot or continuous ADC measurements. | ||||||
|   /// @param channel The ADC1 channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc. |   /// @param unit The ADC unit to use (ADC_UNIT_1 or ADC_UNIT_2). | ||||||
|  |   /// @param channel The ADC channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc. | ||||||
|   void set_channel(adc_unit_t unit, adc_channel_t channel) { |   void set_channel(adc_unit_t unit, adc_channel_t channel) { | ||||||
|     this->adc_unit_ = unit; |     this->adc_unit_ = unit; | ||||||
|     this->channel_ = channel; |     this->channel_ = channel; | ||||||
|   | |||||||
| @@ -43,9 +43,10 @@ void ADCSensor::setup() { | |||||||
|     adc_oneshot_unit_init_cfg_t init_config = {};  // Zero initialize |     adc_oneshot_unit_init_cfg_t init_config = {};  // Zero initialize | ||||||
|     init_config.unit_id = this->adc_unit_; |     init_config.unit_id = this->adc_unit_; | ||||||
|     init_config.ulp_mode = ADC_ULP_MODE_DISABLE; |     init_config.ulp_mode = ADC_ULP_MODE_DISABLE; | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|     init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; |     init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; | ||||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || | ||||||
|  |         // USE_ESP32_VARIANT_ESP32H2 | ||||||
|     esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); |     esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); | ||||||
|     if (err != ESP_OK) { |     if (err != ESP_OK) { | ||||||
|       ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); |       ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); | ||||||
| @@ -74,7 +75,8 @@ void ADCSensor::setup() { | |||||||
|     adc_cali_handle_t handle = nullptr; |     adc_cali_handle_t handle = nullptr; | ||||||
|     esp_err_t err; |     esp_err_t err; | ||||||
|  |  | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
|  |     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|     // RISC-V variants and S3 use curve fitting calibration |     // RISC-V variants and S3 use curve fitting calibration | ||||||
|     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first |     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first | ||||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||||
| @@ -111,7 +113,7 @@ void ADCSensor::setup() { | |||||||
|       ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); |       ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||||
|       this->setup_flags_.calibration_complete = false; |       this->setup_flags_.calibration_complete = false; | ||||||
|     } |     } | ||||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2 | #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->setup_flags_.init_complete = true; |   this->setup_flags_.init_complete = true; | ||||||
| @@ -185,11 +187,12 @@ float ADCSensor::sample_fixed_attenuation_() { | |||||||
|     } else { |     } else { | ||||||
|       ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); |       ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); | ||||||
|       if (this->calibration_handle_ != nullptr) { |       if (this->calibration_handle_ != nullptr) { | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
|  |     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|         adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); |         adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||||
| #else   // Other ESP32 variants use line fitting calibration | #else   // Other ESP32 variants use line fitting calibration | ||||||
|         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); |         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2 | #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 | ||||||
|         this->calibration_handle_ = nullptr; |         this->calibration_handle_ = nullptr; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -217,7 +220,8 @@ float ADCSensor::sample_autorange_() { | |||||||
|     // Need to recalibrate for the new attenuation |     // Need to recalibrate for the new attenuation | ||||||
|     if (this->calibration_handle_ != nullptr) { |     if (this->calibration_handle_ != nullptr) { | ||||||
|       // Delete old calibration handle |       // Delete old calibration handle | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
|  |     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|       adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); |       adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||||
| #else | #else | ||||||
|       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); |       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||||
| @@ -228,7 +232,8 @@ float ADCSensor::sample_autorange_() { | |||||||
|     // Create new calibration handle for this attenuation |     // Create new calibration handle for this attenuation | ||||||
|     adc_cali_handle_t handle = nullptr; |     adc_cali_handle_t handle = nullptr; | ||||||
|  |  | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
|  |     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|     adc_cali_curve_fitting_config_t cali_config = {}; |     adc_cali_curve_fitting_config_t cali_config = {}; | ||||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||||
|     cali_config.chan = this->channel_; |     cali_config.chan = this->channel_; | ||||||
| @@ -256,7 +261,8 @@ float ADCSensor::sample_autorange_() { | |||||||
|     if (err != ESP_OK) { |     if (err != ESP_OK) { | ||||||
|       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); |       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); | ||||||
|       if (handle != nullptr) { |       if (handle != nullptr) { | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
|  |     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|         adc_cali_delete_scheme_curve_fitting(handle); |         adc_cali_delete_scheme_curve_fitting(handle); | ||||||
| #else | #else | ||||||
|         adc_cali_delete_scheme_line_fitting(handle); |         adc_cali_delete_scheme_line_fitting(handle); | ||||||
| @@ -275,7 +281,8 @@ float ADCSensor::sample_autorange_() { | |||||||
|         voltage = raw * 3.3f / 4095.0f; |         voltage = raw * 3.3f / 4095.0f; | ||||||
|       } |       } | ||||||
|       // Clean up calibration handle |       // Clean up calibration handle | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
|  |     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|       adc_cali_delete_scheme_curve_fitting(handle); |       adc_cali_delete_scheme_curve_fitting(handle); | ||||||
| #else | #else | ||||||
|       adc_cali_delete_scheme_line_fitting(handle); |       adc_cali_delete_scheme_line_fitting(handle); | ||||||
|   | |||||||
| @@ -42,18 +42,37 @@ static const char *const TAG = "api.connection"; | |||||||
| static const int CAMERA_STOP_STREAM = 5000; | static const int CAMERA_STOP_STREAM = 5000; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| // Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object | #ifdef USE_DEVICES | ||||||
|  | // Helper macro for entity command handlers - gets entity by key and device_id, returns if not found, and creates call | ||||||
|  | // object | ||||||
|  | #define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ | ||||||
|  |   entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \ | ||||||
|  |   if ((entity_var) == nullptr) \ | ||||||
|  |     return; \ | ||||||
|  |   auto call = (entity_var)->make_call(); | ||||||
|  |  | ||||||
|  | // Helper macro for entity command handlers that don't use make_call() - gets entity by key and device_id and returns if | ||||||
|  | // not found | ||||||
|  | #define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ | ||||||
|  |   entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \ | ||||||
|  |   if ((entity_var) == nullptr) \ | ||||||
|  |     return; | ||||||
|  | #else  // No device support, use simpler macros | ||||||
|  | // Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call | ||||||
|  | // object | ||||||
| #define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ | #define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ | ||||||
|   entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ |   entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ | ||||||
|   if ((entity_var) == nullptr) \ |   if ((entity_var) == nullptr) \ | ||||||
|     return; \ |     return; \ | ||||||
|   auto call = (entity_var)->make_call(); |   auto call = (entity_var)->make_call(); | ||||||
|  |  | ||||||
| // Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found | // Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if | ||||||
|  | // not found | ||||||
| #define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ | #define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ | ||||||
|   entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ |   entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ | ||||||
|   if ((entity_var) == nullptr) \ |   if ((entity_var) == nullptr) \ | ||||||
|     return; |     return; | ||||||
|  | #endif  // USE_DEVICES | ||||||
|  |  | ||||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { |     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||||
| @@ -183,7 +202,8 @@ void APIConnection::loop() { | |||||||
|   } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { |   } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { | ||||||
|     // Only send ping if we're not disconnecting |     // Only send ping if we're not disconnecting | ||||||
|     ESP_LOGVV(TAG, "Sending keepalive PING"); |     ESP_LOGVV(TAG, "Sending keepalive PING"); | ||||||
|     this->flags_.sent_ping = this->send_message(PingRequest()); |     PingRequest req; | ||||||
|  |     this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); | ||||||
|     if (!this->flags_.sent_ping) { |     if (!this->flags_.sent_ping) { | ||||||
|       // If we can't send the ping request directly (tx_buffer full), |       // If we can't send the ping request directly (tx_buffer full), | ||||||
|       // schedule it at the front of the batch so it will be sent with priority |       // schedule it at the front of the batch so it will be sent with priority | ||||||
| @@ -232,7 +252,7 @@ void APIConnection::loop() { | |||||||
|       resp.entity_id = it.entity_id; |       resp.entity_id = it.entity_id; | ||||||
|       resp.attribute = it.attribute.value(); |       resp.attribute = it.attribute.value(); | ||||||
|       resp.once = it.once; |       resp.once = it.once; | ||||||
|       if (this->send_message(resp)) { |       if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { | ||||||
|         state_subs_at_++; |         state_subs_at_++; | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
| @@ -1104,9 +1124,9 @@ bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertiseme | |||||||
|       manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); |       manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); | ||||||
|       manufacturer_data.data.clear(); |       manufacturer_data.data.clear(); | ||||||
|     } |     } | ||||||
|     return this->send_message(resp); |     return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); | ||||||
|   } |   } | ||||||
|   return this->send_message(msg); |   return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
| void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { | void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { | ||||||
|   bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); |   bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ class APIConnection : public APIServerConnection { | |||||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { |   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||||
|     if (!this->flags_.service_call_subscription) |     if (!this->flags_.service_call_subscription) | ||||||
|       return; |       return; | ||||||
|     this->send_message(call); |     this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); | ||||||
|   } |   } | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; |   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||||
| @@ -133,7 +133,7 @@ class APIConnection : public APIServerConnection { | |||||||
| #ifdef USE_HOMEASSISTANT_TIME | #ifdef USE_HOMEASSISTANT_TIME | ||||||
|   void send_time_request() { |   void send_time_request() { | ||||||
|     GetTimeRequest req; |     GetTimeRequest req; | ||||||
|     this->send_message(req); |     this->send_message(req, GetTimeRequest::MESSAGE_TYPE); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -598,32 +598,32 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | |||||||
|  |  | ||||||
| void APIServerConnection::on_hello_request(const HelloRequest &msg) { | void APIServerConnection::on_hello_request(const HelloRequest &msg) { | ||||||
|   HelloResponse ret = this->hello(msg); |   HelloResponse ret = this->hello(msg); | ||||||
|   if (!this->send_message(ret)) { |   if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) { | ||||||
|     this->on_fatal_error(); |     this->on_fatal_error(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void APIServerConnection::on_connect_request(const ConnectRequest &msg) { | void APIServerConnection::on_connect_request(const ConnectRequest &msg) { | ||||||
|   ConnectResponse ret = this->connect(msg); |   ConnectResponse ret = this->connect(msg); | ||||||
|   if (!this->send_message(ret)) { |   if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) { | ||||||
|     this->on_fatal_error(); |     this->on_fatal_error(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { | void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { | ||||||
|   DisconnectResponse ret = this->disconnect(msg); |   DisconnectResponse ret = this->disconnect(msg); | ||||||
|   if (!this->send_message(ret)) { |   if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) { | ||||||
|     this->on_fatal_error(); |     this->on_fatal_error(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void APIServerConnection::on_ping_request(const PingRequest &msg) { | void APIServerConnection::on_ping_request(const PingRequest &msg) { | ||||||
|   PingResponse ret = this->ping(msg); |   PingResponse ret = this->ping(msg); | ||||||
|   if (!this->send_message(ret)) { |   if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) { | ||||||
|     this->on_fatal_error(); |     this->on_fatal_error(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { | void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { | ||||||
|   if (this->check_connection_setup_()) { |   if (this->check_connection_setup_()) { | ||||||
|     DeviceInfoResponse ret = this->device_info(msg); |     DeviceInfoResponse ret = this->device_info(msg); | ||||||
|     if (!this->send_message(ret)) { |     if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) { | ||||||
|       this->on_fatal_error(); |       this->on_fatal_error(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -657,7 +657,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc | |||||||
| void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | ||||||
|   if (this->check_connection_setup_()) { |   if (this->check_connection_setup_()) { | ||||||
|     GetTimeResponse ret = this->get_time(msg); |     GetTimeResponse ret = this->get_time(msg); | ||||||
|     if (!this->send_message(ret)) { |     if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) { | ||||||
|       this->on_fatal_error(); |       this->on_fatal_error(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -673,7 +673,7 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest | |||||||
| void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | ||||||
|   if (this->check_authenticated_()) { |   if (this->check_authenticated_()) { | ||||||
|     NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); |     NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); | ||||||
|     if (!this->send_message(ret)) { |     if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) { | ||||||
|       this->on_fatal_error(); |       this->on_fatal_error(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -867,7 +867,7 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request( | |||||||
|     const SubscribeBluetoothConnectionsFreeRequest &msg) { |     const SubscribeBluetoothConnectionsFreeRequest &msg) { | ||||||
|   if (this->check_authenticated_()) { |   if (this->check_authenticated_()) { | ||||||
|     BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); |     BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); | ||||||
|     if (!this->send_message(ret)) { |     if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) { | ||||||
|       this->on_fatal_error(); |       this->on_fatal_error(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -899,7 +899,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo | |||||||
| void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { | void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { | ||||||
|   if (this->check_authenticated_()) { |   if (this->check_authenticated_()) { | ||||||
|     VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); |     VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); | ||||||
|     if (!this->send_message(ret)) { |     if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) { | ||||||
|       this->on_fatal_error(); |       this->on_fatal_error(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService { | |||||||
|  public: |  public: | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   template<typename T> bool send_message(const T &msg) { |   bool send_message(const ProtoMessage &msg, uint8_t message_type) { | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|     this->log_send_message_(msg.message_name(), msg.dump()); |     this->log_send_message_(msg.message_name(), msg.dump()); | ||||||
| #endif | #endif | ||||||
|     return this->send_message_(msg, T::MESSAGE_TYPE); |     return this->send_message_(msg, message_type); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   virtual void on_hello_request(const HelloRequest &value){}; |   virtual void on_hello_request(const HelloRequest &value){}; | ||||||
|   | |||||||
| @@ -428,7 +428,8 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | |||||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); |       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||||
|       this->set_noise_psk(psk); |       this->set_noise_psk(psk); | ||||||
|       for (auto &c : this->clients_) { |       for (auto &c : this->clients_) { | ||||||
|         c->send_message(DisconnectRequest()); |         DisconnectRequest req; | ||||||
|  |         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -461,7 +462,8 @@ void APIServer::on_shutdown() { | |||||||
|  |  | ||||||
|   // Send disconnect requests to all connected clients |   // Send disconnect requests to all connected clients | ||||||
|   for (auto &c : this->clients_) { |   for (auto &c : this->clients_) { | ||||||
|     if (!c->send_message(DisconnectRequest())) { |     DisconnectRequest req; | ||||||
|  |     if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { | ||||||
|       // If we can't send the disconnect request directly (tx_buffer full), |       // If we can't send the disconnect request directly (tx_buffer full), | ||||||
|       // schedule it at the front of the batch so it will be sent with priority |       // schedule it at the front of the batch so it will be sent with priority | ||||||
|       c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, |       c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie | |||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
| bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||||
|   auto resp = service->encode_list_service_response(); |   auto resp = service->encode_list_service_response(); | ||||||
|   return this->client_->send_message(resp); |   return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       resp.data.reserve(param->read.value_len); |       resp.data.reserve(param->read.value_len); | ||||||
|       // Use bulk insert instead of individual push_backs |       // Use bulk insert instead of individual push_backs | ||||||
|       resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); |       resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); | ||||||
|       this->proxy_->get_api_connection()->send_message(resp); |       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_WRITE_CHAR_EVT: |     case ESP_GATTC_WRITE_CHAR_EVT: | ||||||
| @@ -89,7 +89,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       api::BluetoothGATTWriteResponse resp; |       api::BluetoothGATTWriteResponse resp; | ||||||
|       resp.address = this->address_; |       resp.address = this->address_; | ||||||
|       resp.handle = param->write.handle; |       resp.handle = param->write.handle; | ||||||
|       this->proxy_->get_api_connection()->send_message(resp); |       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { |     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||||
| @@ -103,7 +103,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       api::BluetoothGATTNotifyResponse resp; |       api::BluetoothGATTNotifyResponse resp; | ||||||
|       resp.address = this->address_; |       resp.address = this->address_; | ||||||
|       resp.handle = param->unreg_for_notify.handle; |       resp.handle = param->unreg_for_notify.handle; | ||||||
|       this->proxy_->get_api_connection()->send_message(resp); |       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { |     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||||
| @@ -116,7 +116,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       api::BluetoothGATTNotifyResponse resp; |       api::BluetoothGATTNotifyResponse resp; | ||||||
|       resp.address = this->address_; |       resp.address = this->address_; | ||||||
|       resp.handle = param->reg_for_notify.handle; |       resp.handle = param->reg_for_notify.handle; | ||||||
|       this->proxy_->get_api_connection()->send_message(resp); |       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_NOTIFY_EVT: { |     case ESP_GATTC_NOTIFY_EVT: { | ||||||
| @@ -128,7 +128,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       resp.data.reserve(param->notify.value_len); |       resp.data.reserve(param->notify.value_len); | ||||||
|       // Use bulk insert instead of individual push_backs |       // Use bulk insert instead of individual push_backs | ||||||
|       resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); |       resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); | ||||||
|       this->proxy_->get_api_connection()->send_message(resp); |       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     default: |     default: | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta | |||||||
|   resp.state = static_cast<api::enums::BluetoothScannerState>(state); |   resp.state = static_cast<api::enums::BluetoothScannerState>(state); | ||||||
|   resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE |   resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE | ||||||
|                                                : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; |                                                : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; | ||||||
|   this->api_connection_->send_message(resp); |   this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_BLE_DEVICE | #ifdef USE_ESP32_BLE_DEVICE | ||||||
| @@ -111,7 +111,7 @@ void BluetoothProxy::flush_pending_advertisements() { | |||||||
|  |  | ||||||
|   api::BluetoothLERawAdvertisementsResponse resp; |   api::BluetoothLERawAdvertisementsResponse resp; | ||||||
|   resp.advertisements.swap(batch_buffer); |   resp.advertisements.swap(batch_buffer); | ||||||
|   this->api_connection_->send_message(resp); |   this->api_connection_->send_message(resp, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_BLE_DEVICE | #ifdef USE_ESP32_BLE_DEVICE | ||||||
| @@ -150,7 +150,7 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi | |||||||
|     manufacturer_data.data.assign(data.data.begin(), data.data.end()); |     manufacturer_data.data.assign(data.data.begin(), data.data.end()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->api_connection_->send_message(resp); |   this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
| #endif  // USE_ESP32_BLE_DEVICE | #endif  // USE_ESP32_BLE_DEVICE | ||||||
|  |  | ||||||
| @@ -309,7 +309,7 @@ void BluetoothProxy::loop() { | |||||||
|         service_resp.characteristics.push_back(std::move(characteristic_resp)); |         service_resp.characteristics.push_back(std::move(characteristic_resp)); | ||||||
|       } |       } | ||||||
|       resp.services.push_back(std::move(service_resp)); |       resp.services.push_back(std::move(service_resp)); | ||||||
|       this->api_connection_->send_message(resp); |       this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -460,7 +460,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|       call.success = ret == ESP_OK; |       call.success = ret == ESP_OK; | ||||||
|       call.error = ret; |       call.error = ret; | ||||||
|  |  | ||||||
|       this->api_connection_->send_message(call); |       this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); | ||||||
|  |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| @@ -582,7 +582,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui | |||||||
|   call.connected = connected; |   call.connected = connected; | ||||||
|   call.mtu = mtu; |   call.mtu = mtu; | ||||||
|   call.error = error; |   call.error = error; | ||||||
|   this->api_connection_->send_message(call); |   this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
| void BluetoothProxy::send_connections_free() { | void BluetoothProxy::send_connections_free() { | ||||||
|   if (this->api_connection_ == nullptr) |   if (this->api_connection_ == nullptr) | ||||||
| @@ -595,7 +595,7 @@ void BluetoothProxy::send_connections_free() { | |||||||
|       call.allocated.push_back(connection->address_); |       call.allocated.push_back(connection->address_); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   this->api_connection_->send_message(call); |   this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::send_gatt_services_done(uint64_t address) { | void BluetoothProxy::send_gatt_services_done(uint64_t address) { | ||||||
| @@ -603,7 +603,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { | |||||||
|     return; |     return; | ||||||
|   api::BluetoothGATTGetServicesDoneResponse call; |   api::BluetoothGATTGetServicesDoneResponse call; | ||||||
|   call.address = address; |   call.address = address; | ||||||
|   this->api_connection_->send_message(call); |   this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { | void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { | ||||||
| @@ -613,7 +613,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ | |||||||
|   call.address = address; |   call.address = address; | ||||||
|   call.handle = handle; |   call.handle = handle; | ||||||
|   call.error = error; |   call.error = error; | ||||||
|   this->api_connection_->send_message(call); |   this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { | void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { | ||||||
| @@ -622,7 +622,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ | |||||||
|   call.paired = paired; |   call.paired = paired; | ||||||
|   call.error = error; |   call.error = error; | ||||||
|  |  | ||||||
|   this->api_connection_->send_message(call); |   this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { | void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { | ||||||
| @@ -631,7 +631,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e | |||||||
|   call.success = success; |   call.success = success; | ||||||
|   call.error = error; |   call.error = error; | ||||||
|  |  | ||||||
|   this->api_connection_->send_message(call); |   this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { | void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ from esphome.const import ( | |||||||
|     CONF_UNIT_OF_MEASUREMENT, |     CONF_UNIT_OF_MEASUREMENT, | ||||||
|     CONF_VALUE, |     CONF_VALUE, | ||||||
|     CONF_WEB_SERVER, |     CONF_WEB_SERVER, | ||||||
|  |     DEVICE_CLASS_ABSOLUTE_HUMIDITY, | ||||||
|     DEVICE_CLASS_APPARENT_POWER, |     DEVICE_CLASS_APPARENT_POWER, | ||||||
|     DEVICE_CLASS_AQI, |     DEVICE_CLASS_AQI, | ||||||
|     DEVICE_CLASS_AREA, |     DEVICE_CLASS_AREA, | ||||||
| @@ -81,6 +82,7 @@ from esphome.cpp_generator import MockObjClass | |||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| DEVICE_CLASSES = [ | DEVICE_CLASSES = [ | ||||||
|  |     DEVICE_CLASS_ABSOLUTE_HUMIDITY, | ||||||
|     DEVICE_CLASS_APPARENT_POWER, |     DEVICE_CLASS_APPARENT_POWER, | ||||||
|     DEVICE_CLASS_AQI, |     DEVICE_CLASS_AQI, | ||||||
|     DEVICE_CLASS_AREA, |     DEVICE_CLASS_AREA, | ||||||
|   | |||||||
| @@ -41,6 +41,7 @@ from esphome.const import ( | |||||||
|     CONF_VALUE, |     CONF_VALUE, | ||||||
|     CONF_WEB_SERVER, |     CONF_WEB_SERVER, | ||||||
|     CONF_WINDOW_SIZE, |     CONF_WINDOW_SIZE, | ||||||
|  |     DEVICE_CLASS_ABSOLUTE_HUMIDITY, | ||||||
|     DEVICE_CLASS_APPARENT_POWER, |     DEVICE_CLASS_APPARENT_POWER, | ||||||
|     DEVICE_CLASS_AQI, |     DEVICE_CLASS_AQI, | ||||||
|     DEVICE_CLASS_AREA, |     DEVICE_CLASS_AREA, | ||||||
| @@ -107,6 +108,7 @@ from esphome.util import Registry | |||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| DEVICE_CLASSES = [ | DEVICE_CLASSES = [ | ||||||
|  |     DEVICE_CLASS_ABSOLUTE_HUMIDITY, | ||||||
|     DEVICE_CLASS_APPARENT_POWER, |     DEVICE_CLASS_APPARENT_POWER, | ||||||
|     DEVICE_CLASS_AQI, |     DEVICE_CLASS_AQI, | ||||||
|     DEVICE_CLASS_AREA, |     DEVICE_CLASS_AREA, | ||||||
|   | |||||||
| @@ -223,7 +223,8 @@ void VoiceAssistant::loop() { | |||||||
|       msg.wake_word_phrase = this->wake_word_; |       msg.wake_word_phrase = this->wake_word_; | ||||||
|       this->wake_word_ = ""; |       this->wake_word_ = ""; | ||||||
|  |  | ||||||
|       if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { |       if (this->api_client_ == nullptr || | ||||||
|  |           !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { | ||||||
|         ESP_LOGW(TAG, "Could not request start"); |         ESP_LOGW(TAG, "Could not request start"); | ||||||
|         this->error_trigger_->trigger("not-connected", "Could not request start"); |         this->error_trigger_->trigger("not-connected", "Could not request start"); | ||||||
|         this->continuous_ = false; |         this->continuous_ = false; | ||||||
| @@ -245,7 +246,7 @@ void VoiceAssistant::loop() { | |||||||
|         if (this->audio_mode_ == AUDIO_MODE_API) { |         if (this->audio_mode_ == AUDIO_MODE_API) { | ||||||
|           api::VoiceAssistantAudio msg; |           api::VoiceAssistantAudio msg; | ||||||
|           msg.data.assign((char *) this->send_buffer_, read_bytes); |           msg.data.assign((char *) this->send_buffer_, read_bytes); | ||||||
|           this->api_client_->send_message(msg); |           this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); | ||||||
|         } else { |         } else { | ||||||
|           if (!this->udp_socket_running_) { |           if (!this->udp_socket_running_) { | ||||||
|             if (!this->start_udp_socket_()) { |             if (!this->start_udp_socket_()) { | ||||||
| @@ -331,7 +332,7 @@ void VoiceAssistant::loop() { | |||||||
|  |  | ||||||
|           api::VoiceAssistantAnnounceFinished msg; |           api::VoiceAssistantAnnounceFinished msg; | ||||||
|           msg.success = true; |           msg.success = true; | ||||||
|           this->api_client_->send_message(msg); |           this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -580,7 +581,7 @@ void VoiceAssistant::signal_stop_() { | |||||||
|   ESP_LOGD(TAG, "Signaling stop"); |   ESP_LOGD(TAG, "Signaling stop"); | ||||||
|   api::VoiceAssistantRequest msg; |   api::VoiceAssistantRequest msg; | ||||||
|   msg.start = false; |   msg.start = false; | ||||||
|   this->api_client_->send_message(msg); |   this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| void VoiceAssistant::start_playback_timeout_() { | void VoiceAssistant::start_playback_timeout_() { | ||||||
| @@ -590,7 +591,7 @@ void VoiceAssistant::start_playback_timeout_() { | |||||||
|  |  | ||||||
|     api::VoiceAssistantAnnounceFinished msg; |     api::VoiceAssistantAnnounceFinished msg; | ||||||
|     msg.success = true; |     msg.success = true; | ||||||
|     this->api_client_->send_message(msg); |     this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1193,6 +1193,7 @@ UNIT_WATT = "W" | |||||||
| UNIT_WATT_HOURS = "Wh" | UNIT_WATT_HOURS = "Wh" | ||||||
|  |  | ||||||
| # device classes | # device classes | ||||||
|  | DEVICE_CLASS_ABSOLUTE_HUMIDITY = "absolute_humidity" | ||||||
| DEVICE_CLASS_APPARENT_POWER = "apparent_power" | DEVICE_CLASS_APPARENT_POWER = "apparent_power" | ||||||
| DEVICE_CLASS_AQI = "aqi" | DEVICE_CLASS_AQI = "aqi" | ||||||
| DEVICE_CLASS_AREA = "area" | DEVICE_CLASS_AREA = "area" | ||||||
|   | |||||||
| @@ -368,8 +368,19 @@ class Application { | |||||||
|  |  | ||||||
|   uint8_t get_app_state() const { return this->app_state_; } |   uint8_t get_app_state() const { return this->app_state_; } | ||||||
|  |  | ||||||
| // Helper macro for entity getter method declarations - reduces code duplication | // Helper macro for entity getter method declarations | ||||||
| // When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter | #ifdef USE_DEVICES | ||||||
|  | #define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ | ||||||
|  |   entity_type *get_##entity_name##_by_key(uint32_t key, uint32_t device_id, bool include_internal = false) { \ | ||||||
|  |     for (auto *obj : this->entities_member##_) { \ | ||||||
|  |       if (obj->get_object_id_hash() == key && obj->get_device_id() == device_id && \ | ||||||
|  |           (include_internal || !obj->is_internal())) \ | ||||||
|  |         return obj; \ | ||||||
|  |     } \ | ||||||
|  |     return nullptr; \ | ||||||
|  |   } | ||||||
|  |   const std::vector<Device *> &get_devices() { return this->devices_; } | ||||||
|  | #else | ||||||
| #define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ | #define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ | ||||||
|   entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \ |   entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \ | ||||||
|     for (auto *obj : this->entities_member##_) { \ |     for (auto *obj : this->entities_member##_) { \ | ||||||
| @@ -378,10 +389,7 @@ class Application { | |||||||
|     } \ |     } \ | ||||||
|     return nullptr; \ |     return nullptr; \ | ||||||
|   } |   } | ||||||
|  | #endif  // USE_DEVICES | ||||||
| #ifdef USE_DEVICES |  | ||||||
|   const std::vector<Device *> &get_devices() { return this->devices_; } |  | ||||||
| #endif |  | ||||||
| #ifdef USE_AREAS | #ifdef USE_AREAS | ||||||
|   const std::vector<Area *> &get_areas() { return this->areas_; } |   const std::vector<Area *> &get_areas() { return this->areas_; } | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -198,9 +198,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy | |||||||
|  |  | ||||||
|         # Get device name if entity is on a sub-device |         # Get device name if entity is on a sub-device | ||||||
|         device_name = None |         device_name = None | ||||||
|  |         device_id = ""  # Empty string for main device | ||||||
|         if CONF_DEVICE_ID in config: |         if CONF_DEVICE_ID in config: | ||||||
|             device_id_obj = config[CONF_DEVICE_ID] |             device_id_obj = config[CONF_DEVICE_ID] | ||||||
|             device_name = device_id_obj.id |             device_name = device_id_obj.id | ||||||
|  |             # Use the device ID string directly for uniqueness | ||||||
|  |             device_id = device_id_obj.id | ||||||
|  |  | ||||||
|         # Calculate what object_id will actually be used |         # Calculate what object_id will actually be used | ||||||
|         # This handles empty names correctly by using device/friendly names |         # This handles empty names correctly by using device/friendly names | ||||||
| @@ -209,11 +212,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Check for duplicates |         # Check for duplicates | ||||||
|         unique_key = (platform, name_key) |         unique_key = (device_id, platform, name_key) | ||||||
|         if unique_key in CORE.unique_ids: |         if unique_key in CORE.unique_ids: | ||||||
|  |             device_prefix = f" on device '{device_id}'" if device_id else "" | ||||||
|             raise cv.Invalid( |             raise cv.Invalid( | ||||||
|                 f"Duplicate {platform} entity with name '{entity_name}' found. " |                 f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " | ||||||
|                 f"Each entity must have a unique name within its platform across all devices." |                 f"Each entity on a device must have a unique name within its platform." | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Add to tracking set |         # Add to tracking set | ||||||
|   | |||||||
| @@ -1713,13 +1713,12 @@ static const char *const TAG = "api.service"; | |||||||
|     hpp += " public:\n" |     hpp += " public:\n" | ||||||
|     hpp += "#endif\n\n" |     hpp += "#endif\n\n" | ||||||
|  |  | ||||||
|     # Add generic send_message method |     # Add non-template send_message method | ||||||
|     hpp += "  template<typename T>\n" |     hpp += "  bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n" | ||||||
|     hpp += "  bool send_message(const T &msg) {\n" |  | ||||||
|     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" |     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" | ||||||
|     hpp += "    this->log_send_message_(msg.message_name(), msg.dump());\n" |     hpp += "    this->log_send_message_(msg.message_name(), msg.dump());\n" | ||||||
|     hpp += "#endif\n" |     hpp += "#endif\n" | ||||||
|     hpp += "    return this->send_message_(msg, T::MESSAGE_TYPE);\n" |     hpp += "    return this->send_message_(msg, message_type);\n" | ||||||
|     hpp += "  }\n\n" |     hpp += "  }\n\n" | ||||||
|  |  | ||||||
|     # Add logging helper method implementation to cpp |     # Add logging helper method implementation to cpp | ||||||
| @@ -1805,7 +1804,9 @@ static const char *const TAG = "api.service"; | |||||||
|                 handler_body = f"this->{func}(msg);\n" |                 handler_body = f"this->{func}(msg);\n" | ||||||
|             else: |             else: | ||||||
|                 handler_body = f"{ret} ret = this->{func}(msg);\n" |                 handler_body = f"{ret} ret = this->{func}(msg);\n" | ||||||
|                 handler_body += "if (!this->send_message(ret)) {\n" |                 handler_body += ( | ||||||
|  |                     f"if (!this->send_message(ret, {ret}::MESSAGE_TYPE)) {{\n" | ||||||
|  |                 ) | ||||||
|                 handler_body += "  this->on_fatal_error();\n" |                 handler_body += "  this->on_fatal_error();\n" | ||||||
|                 handler_body += "}\n" |                 handler_body += "}\n" | ||||||
|  |  | ||||||
| @@ -1818,7 +1819,7 @@ static const char *const TAG = "api.service"; | |||||||
|                 body += f"this->{func}(msg);\n" |                 body += f"this->{func}(msg);\n" | ||||||
|             else: |             else: | ||||||
|                 body += f"{ret} ret = this->{func}(msg);\n" |                 body += f"{ret} ret = this->{func}(msg);\n" | ||||||
|                 body += "if (!this->send_message(ret)) {\n" |                 body += f"if (!this->send_message(ret, {ret}::MESSAGE_TYPE)) {{\n" | ||||||
|                 body += "  this->on_fatal_error();\n" |                 body += "  this->on_fatal_error();\n" | ||||||
|                 body += "}\n" |                 body += "}\n" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -530,27 +530,26 @@ def get_components_from_integration_fixtures() -> set[str]: | |||||||
|     Returns: |     Returns: | ||||||
|         Set of component names used in integration test fixtures |         Set of component names used in integration test fixtures | ||||||
|     """ |     """ | ||||||
|     import yaml |     from esphome import yaml_util | ||||||
|  |  | ||||||
|     components: set[str] = set() |     components: set[str] = set() | ||||||
|     fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures" |     fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures" | ||||||
|  |  | ||||||
|     for yaml_file in fixtures_dir.glob("*.yaml"): |     for yaml_file in fixtures_dir.glob("*.yaml"): | ||||||
|         with open(yaml_file) as f: |         config: dict[str, any] | None = yaml_util.load_yaml(str(yaml_file)) | ||||||
|             config: dict[str, any] | None = yaml.safe_load(f) |         if not config: | ||||||
|             if not config: |             continue | ||||||
|  |  | ||||||
|  |         # Add all top-level component keys | ||||||
|  |         components.update(config.keys()) | ||||||
|  |  | ||||||
|  |         # Add platform components (e.g., output.template) | ||||||
|  |         for value in config.values(): | ||||||
|  |             if not isinstance(value, list): | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             # Add all top-level component keys |             for item in value: | ||||||
|             components.update(config.keys()) |                 if isinstance(item, dict) and "platform" in item: | ||||||
|  |                     components.add(item["platform"]) | ||||||
|             # Add platform components (e.g., output.template) |  | ||||||
|             for value in config.values(): |  | ||||||
|                 if not isinstance(value, list): |  | ||||||
|                     continue |  | ||||||
|  |  | ||||||
|                 for item in value: |  | ||||||
|                     if isinstance(item, dict) and "platform" in item: |  | ||||||
|                         components.add(item["platform"]) |  | ||||||
|  |  | ||||||
|     return components |     return components | ||||||
|   | |||||||
| @@ -54,3 +54,45 @@ sensor: | |||||||
|     device_id: smart_switch_device |     device_id: smart_switch_device | ||||||
|     lambda: return 4.0; |     lambda: return 4.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  | # Switches with the same name on different devices to test device_id lookup | ||||||
|  | switch: | ||||||
|  |   # Switch with no device_id (defaults to 0) | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Switch | ||||||
|  |     id: test_switch_main | ||||||
|  |     optimistic: true | ||||||
|  |     turn_on_action: | ||||||
|  |       - logger.log: "Turning on Test Switch on Main Device (no device_id)" | ||||||
|  |     turn_off_action: | ||||||
|  |       - logger.log: "Turning off Test Switch on Main Device (no device_id)" | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Switch | ||||||
|  |     device_id: light_controller_device | ||||||
|  |     id: test_switch_light_controller | ||||||
|  |     optimistic: true | ||||||
|  |     turn_on_action: | ||||||
|  |       - logger.log: "Turning on Test Switch on Light Controller" | ||||||
|  |     turn_off_action: | ||||||
|  |       - logger.log: "Turning off Test Switch on Light Controller" | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Switch | ||||||
|  |     device_id: temp_sensor_device | ||||||
|  |     id: test_switch_temp_sensor | ||||||
|  |     optimistic: true | ||||||
|  |     turn_on_action: | ||||||
|  |       - logger.log: "Turning on Test Switch on Temperature Sensor" | ||||||
|  |     turn_off_action: | ||||||
|  |       - logger.log: "Turning off Test Switch on Temperature Sensor" | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Switch | ||||||
|  |     device_id: motion_detector_device | ||||||
|  |     id: test_switch_motion_detector | ||||||
|  |     optimistic: true | ||||||
|  |     turn_on_action: | ||||||
|  |       - logger.log: "Turning on Test Switch on Motion Detector" | ||||||
|  |     turn_off_action: | ||||||
|  |       - logger.log: "Turning off Test Switch on Motion Detector" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| esphome: | esphome: | ||||||
|   name: duplicate-entities-test |   name: duplicate-entities-test | ||||||
|   # Define devices to test multi-device unique name validation |   # Define devices to test multi-device duplicate handling | ||||||
|   devices: |   devices: | ||||||
|     - id: controller_1 |     - id: controller_1 | ||||||
|       name: Controller 1 |       name: Controller 1 | ||||||
| @@ -13,31 +13,31 @@ host: | |||||||
| api:  # Port will be automatically injected | api:  # Port will be automatically injected | ||||||
| logger: | logger: | ||||||
| 
 | 
 | ||||||
| # Test that duplicate entity names are NOT allowed on different devices | # Test that duplicate entity names are allowed on different devices | ||||||
| 
 | 
 | ||||||
| # Scenario 1: Different sensor names on different devices (allowed) | # Scenario 1: Same sensor name on different devices (allowed) | ||||||
| sensor: | sensor: | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Temperature Controller 1 |     name: Temperature | ||||||
|     device_id: controller_1 |     device_id: controller_1 | ||||||
|     lambda: return 21.0; |     lambda: return 21.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Temperature Controller 2 |     name: Temperature | ||||||
|     device_id: controller_2 |     device_id: controller_2 | ||||||
|     lambda: return 22.0; |     lambda: return 22.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Temperature Controller 3 |     name: Temperature | ||||||
|     device_id: controller_3 |     device_id: controller_3 | ||||||
|     lambda: return 23.0; |     lambda: return 23.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
|   # Main device sensor (no device_id) |   # Main device sensor (no device_id) | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Temperature Main |     name: Temperature | ||||||
|     lambda: return 20.0; |     lambda: return 20.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
| @@ -47,20 +47,20 @@ sensor: | |||||||
|     lambda: return 60.0; |     lambda: return 60.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
| # Scenario 2: Different binary sensor names on different devices | # Scenario 2: Same binary sensor name on different devices (allowed) | ||||||
| binary_sensor: | binary_sensor: | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Status Controller 1 |     name: Status | ||||||
|     device_id: controller_1 |     device_id: controller_1 | ||||||
|     lambda: return true; |     lambda: return true; | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Status Controller 2 |     name: Status | ||||||
|     device_id: controller_2 |     device_id: controller_2 | ||||||
|     lambda: return false; |     lambda: return false; | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Status Main |     name: Status | ||||||
|     lambda: return true;  # Main device |     lambda: return true;  # Main device | ||||||
| 
 | 
 | ||||||
|   # Different platform can have same name as sensor |   # Different platform can have same name as sensor | ||||||
| @@ -68,43 +68,43 @@ binary_sensor: | |||||||
|     name: Temperature |     name: Temperature | ||||||
|     lambda: return true; |     lambda: return true; | ||||||
| 
 | 
 | ||||||
| # Scenario 3: Different text sensor names on different devices | # Scenario 3: Same text sensor name on different devices | ||||||
| text_sensor: | text_sensor: | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Device Info Controller 1 |     name: Device Info | ||||||
|     device_id: controller_1 |     device_id: controller_1 | ||||||
|     lambda: return {"Controller 1 Active"}; |     lambda: return {"Controller 1 Active"}; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Device Info Controller 2 |     name: Device Info | ||||||
|     device_id: controller_2 |     device_id: controller_2 | ||||||
|     lambda: return {"Controller 2 Active"}; |     lambda: return {"Controller 2 Active"}; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Device Info Main |     name: Device Info | ||||||
|     lambda: return {"Main Device Active"}; |     lambda: return {"Main Device Active"}; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
| 
 | 
 | ||||||
| # Scenario 4: Different switch names on different devices | # Scenario 4: Same switch name on different devices | ||||||
| switch: | switch: | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Power Controller 1 |     name: Power | ||||||
|     device_id: controller_1 |     device_id: controller_1 | ||||||
|     lambda: return false; |     lambda: return false; | ||||||
|     turn_on_action: [] |     turn_on_action: [] | ||||||
|     turn_off_action: [] |     turn_off_action: [] | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Power Controller 2 |     name: Power | ||||||
|     device_id: controller_2 |     device_id: controller_2 | ||||||
|     lambda: return true; |     lambda: return true; | ||||||
|     turn_on_action: [] |     turn_on_action: [] | ||||||
|     turn_off_action: [] |     turn_off_action: [] | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: Power Controller 3 |     name: Power | ||||||
|     device_id: controller_3 |     device_id: controller_3 | ||||||
|     lambda: return false; |     lambda: return false; | ||||||
|     turn_on_action: [] |     turn_on_action: [] | ||||||
| @@ -117,54 +117,26 @@ switch: | |||||||
|     turn_on_action: [] |     turn_on_action: [] | ||||||
|     turn_off_action: [] |     turn_off_action: [] | ||||||
| 
 | 
 | ||||||
| # Scenario 5: Buttons with unique names | # Scenario 5: Empty names on different devices (should use device name) | ||||||
| button: | button: | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: "Reset Controller 1" |     name: "" | ||||||
|     device_id: controller_1 |     device_id: controller_1 | ||||||
|     on_press: [] |     on_press: [] | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: "Reset Controller 2" |     name: "" | ||||||
|     device_id: controller_2 |     device_id: controller_2 | ||||||
|     on_press: [] |     on_press: [] | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: "Reset Main" |     name: "" | ||||||
|     on_press: []  # Main device |     on_press: []  # Main device | ||||||
| 
 | 
 | ||||||
| # Scenario 6: Empty names (should use device names) | # Scenario 6: Special characters in names | ||||||
| select: |  | ||||||
|   - platform: template |  | ||||||
|     name: "" |  | ||||||
|     device_id: controller_1 |  | ||||||
|     options: |  | ||||||
|       - "Option 1" |  | ||||||
|       - "Option 2" |  | ||||||
|     lambda: return {"Option 1"}; |  | ||||||
|     set_action: [] |  | ||||||
| 
 |  | ||||||
|   - platform: template |  | ||||||
|     name: "" |  | ||||||
|     device_id: controller_2 |  | ||||||
|     options: |  | ||||||
|       - "Option 1" |  | ||||||
|       - "Option 2" |  | ||||||
|     lambda: return {"Option 1"}; |  | ||||||
|     set_action: [] |  | ||||||
| 
 |  | ||||||
|   - platform: template |  | ||||||
|     name: ""  # Main device |  | ||||||
|     options: |  | ||||||
|       - "Option 1" |  | ||||||
|       - "Option 2" |  | ||||||
|     lambda: return {"Option 1"}; |  | ||||||
|     set_action: [] |  | ||||||
| 
 |  | ||||||
| # Scenario 7: Special characters in names - now with unique names |  | ||||||
| number: | number: | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: "Temperature Setpoint! Controller 1" |     name: "Temperature Setpoint!" | ||||||
|     device_id: controller_1 |     device_id: controller_1 | ||||||
|     min_value: 10.0 |     min_value: 10.0 | ||||||
|     max_value: 30.0 |     max_value: 30.0 | ||||||
| @@ -173,7 +145,7 @@ number: | |||||||
|     set_action: [] |     set_action: [] | ||||||
| 
 | 
 | ||||||
|   - platform: template |   - platform: template | ||||||
|     name: "Temperature Setpoint! Controller 2" |     name: "Temperature Setpoint!" | ||||||
|     device_id: controller_2 |     device_id: controller_2 | ||||||
|     min_value: 10.0 |     min_value: 10.0 | ||||||
|     max_value: 30.0 |     max_value: 30.0 | ||||||
| @@ -4,7 +4,7 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
| from aioesphomeapi import EntityState | from aioesphomeapi import EntityState, SwitchInfo, SwitchState | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from .types import APIClientConnectedFactory, RunCompiledFunction | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
| @@ -84,23 +84,45 @@ async def test_areas_and_devices( | |||||||
|  |  | ||||||
|         # Subscribe to states to get sensor values |         # Subscribe to states to get sensor values | ||||||
|         loop = asyncio.get_running_loop() |         loop = asyncio.get_running_loop() | ||||||
|         states: dict[int, EntityState] = {} |         states: dict[tuple[int, int], EntityState] = {} | ||||||
|         states_future: asyncio.Future[bool] = loop.create_future() |         # Subscribe to all switch states | ||||||
|  |         switch_state_futures: dict[ | ||||||
|  |             tuple[int, int], asyncio.Future[EntityState] | ||||||
|  |         ] = {}  # (device_id, key) -> future | ||||||
|  |         initial_states_received: set[tuple[int, int]] = set() | ||||||
|  |         initial_states_future: asyncio.Future[bool] = loop.create_future() | ||||||
|  |  | ||||||
|         def on_state(state: EntityState) -> None: |         def on_state(state: EntityState) -> None: | ||||||
|             states[state.key] = state |             state_key = (state.device_id, state.key) | ||||||
|             # Check if we have all expected sensor states |             states[state_key] = state | ||||||
|             if len(states) >= 4 and not states_future.done(): |  | ||||||
|                 states_future.set_result(True) |             initial_states_received.add(state_key) | ||||||
|  |             # Check if we have all initial states | ||||||
|  |             if ( | ||||||
|  |                 len(initial_states_received) >= 8  # 8 entities expected | ||||||
|  |                 and not initial_states_future.done() | ||||||
|  |             ): | ||||||
|  |                 initial_states_future.set_result(True) | ||||||
|  |  | ||||||
|  |             if not initial_states_future.done(): | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             # Resolve the future for this switch if it exists | ||||||
|  |             if ( | ||||||
|  |                 state_key in switch_state_futures | ||||||
|  |                 and not switch_state_futures[state_key].done() | ||||||
|  |                 and isinstance(state, SwitchState) | ||||||
|  |             ): | ||||||
|  |                 switch_state_futures[state_key].set_result(state) | ||||||
|  |  | ||||||
|         client.subscribe_states(on_state) |         client.subscribe_states(on_state) | ||||||
|  |  | ||||||
|         # Wait for sensor states |         # Wait for sensor states | ||||||
|         try: |         try: | ||||||
|             await asyncio.wait_for(states_future, timeout=10.0) |             await asyncio.wait_for(initial_states_future, timeout=10.0) | ||||||
|         except TimeoutError: |         except TimeoutError: | ||||||
|             pytest.fail( |             pytest.fail( | ||||||
|                 f"Did not receive all sensor states within 10 seconds. " |                 f"Did not receive all states within 10 seconds. " | ||||||
|                 f"Received {len(states)} states" |                 f"Received {len(states)} states" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
| @@ -119,3 +141,121 @@ async def test_areas_and_devices( | |||||||
|                     f"{entity.name} has device_id {entity.device_id}, " |                     f"{entity.name} has device_id {entity.device_id}, " | ||||||
|                     f"expected {expected_device_id}" |                     f"expected {expected_device_id}" | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |         all_entities, _ = entities  # Unpack the tuple | ||||||
|  |         switch_entities = [e for e in all_entities if isinstance(e, SwitchInfo)] | ||||||
|  |  | ||||||
|  |         # Find all switches named "Test Switch" | ||||||
|  |         test_switches = [e for e in switch_entities if e.name == "Test Switch"] | ||||||
|  |         assert len(test_switches) == 4, ( | ||||||
|  |             f"Expected 4 'Test Switch' entities, got {len(test_switches)}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify we have switches with different device_ids including one with 0 (main) | ||||||
|  |         switch_device_ids = {s.device_id for s in test_switches} | ||||||
|  |         assert len(switch_device_ids) == 4, ( | ||||||
|  |             "All Test Switch entities should have different device_ids" | ||||||
|  |         ) | ||||||
|  |         assert 0 in switch_device_ids, ( | ||||||
|  |             "Should have a switch with device_id 0 (main device)" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for initial states to be received for all switches | ||||||
|  |         await asyncio.wait_for(initial_states_future, timeout=2.0) | ||||||
|  |  | ||||||
|  |         # Test controlling each switch specifically by device_id | ||||||
|  |         for device_name, device in [ | ||||||
|  |             ("Light Controller", light_controller), | ||||||
|  |             ("Temperature Sensor", temp_sensor), | ||||||
|  |             ("Motion Detector", motion_detector), | ||||||
|  |         ]: | ||||||
|  |             # Find the switch for this specific device | ||||||
|  |             device_switch = next( | ||||||
|  |                 (s for s in test_switches if s.device_id == device.device_id), None | ||||||
|  |             ) | ||||||
|  |             assert device_switch is not None, f"No Test Switch found for {device_name}" | ||||||
|  |  | ||||||
|  |             # Create future for this switch's state change | ||||||
|  |             state_key = (device_switch.device_id, device_switch.key) | ||||||
|  |             switch_state_futures[state_key] = loop.create_future() | ||||||
|  |  | ||||||
|  |             # Turn on the switch with device_id | ||||||
|  |             client.switch_command( | ||||||
|  |                 device_switch.key, True, device_id=device_switch.device_id | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             # Wait for state to change | ||||||
|  |             await asyncio.wait_for(switch_state_futures[state_key], timeout=2.0) | ||||||
|  |  | ||||||
|  |             # Verify the correct switch was turned on | ||||||
|  |             assert states[state_key].state is True, f"{device_name} switch should be on" | ||||||
|  |  | ||||||
|  |             # Create new future for turning off | ||||||
|  |             switch_state_futures[state_key] = loop.create_future() | ||||||
|  |  | ||||||
|  |             # Turn off the switch with device_id | ||||||
|  |             client.switch_command( | ||||||
|  |                 device_switch.key, False, device_id=device_switch.device_id | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             # Wait for state to change | ||||||
|  |             await asyncio.wait_for(switch_state_futures[state_key], timeout=2.0) | ||||||
|  |  | ||||||
|  |             # Verify the correct switch was turned off | ||||||
|  |             assert states[state_key].state is False, ( | ||||||
|  |                 f"{device_name} switch should be off" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Test that controlling a switch with device_id doesn't affect main switch | ||||||
|  |         # Find the main switch (device_id = 0) | ||||||
|  |         main_switch = next((s for s in test_switches if s.device_id == 0), None) | ||||||
|  |         assert main_switch is not None, "No main switch (device_id=0) found" | ||||||
|  |  | ||||||
|  |         # Find a switch with a device_id | ||||||
|  |         device_switch = next( | ||||||
|  |             (s for s in test_switches if s.device_id == light_controller.device_id), | ||||||
|  |             None, | ||||||
|  |         ) | ||||||
|  |         assert device_switch is not None, "No device switch found" | ||||||
|  |  | ||||||
|  |         # Create futures for both switches | ||||||
|  |         main_key = (main_switch.device_id, main_switch.key) | ||||||
|  |         device_key = (device_switch.device_id, device_switch.key) | ||||||
|  |  | ||||||
|  |         # Turn on the main switch first | ||||||
|  |         switch_state_futures[main_key] = loop.create_future() | ||||||
|  |         client.switch_command(main_switch.key, True, device_id=main_switch.device_id) | ||||||
|  |         await asyncio.wait_for(switch_state_futures[main_key], timeout=2.0) | ||||||
|  |         assert states[main_key].state is True, "Main switch should be on" | ||||||
|  |  | ||||||
|  |         # Now turn on the device switch | ||||||
|  |         switch_state_futures[device_key] = loop.create_future() | ||||||
|  |         client.switch_command( | ||||||
|  |             device_switch.key, True, device_id=device_switch.device_id | ||||||
|  |         ) | ||||||
|  |         await asyncio.wait_for(switch_state_futures[device_key], timeout=2.0) | ||||||
|  |  | ||||||
|  |         # Verify device switch is on and main switch is still on | ||||||
|  |         assert states[device_key].state is True, "Device switch should be on" | ||||||
|  |         assert states[main_key].state is True, ( | ||||||
|  |             "Main switch should still be on after turning on device switch" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Turn off the device switch | ||||||
|  |         switch_state_futures[device_key] = loop.create_future() | ||||||
|  |         client.switch_command( | ||||||
|  |             device_switch.key, False, device_id=device_switch.device_id | ||||||
|  |         ) | ||||||
|  |         await asyncio.wait_for(switch_state_futures[device_key], timeout=2.0) | ||||||
|  |  | ||||||
|  |         # Verify device switch is off and main switch is still on | ||||||
|  |         assert states[device_key].state is False, "Device switch should be off" | ||||||
|  |         assert states[main_key].state is True, ( | ||||||
|  |             "Main switch should still be on after turning off device switch" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Clean up - turn off main switch | ||||||
|  |         switch_state_futures[main_key] = loop.create_future() | ||||||
|  |         client.switch_command(main_switch.key, False, device_id=main_switch.device_id) | ||||||
|  |         await asyncio.wait_for(switch_state_futures[main_key], timeout=2.0) | ||||||
|  |         assert states[main_key].state is False, "Main switch should be off" | ||||||
|   | |||||||
| @@ -11,12 +11,12 @@ from .types import APIClientConnectedFactory, RunCompiledFunction | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_duplicate_entities_not_allowed_on_different_devices( | async def test_duplicate_entities_on_different_devices( | ||||||
|     yaml_config: str, |     yaml_config: str, | ||||||
|     run_compiled: RunCompiledFunction, |     run_compiled: RunCompiledFunction, | ||||||
|     api_client_connected: APIClientConnectedFactory, |     api_client_connected: APIClientConnectedFactory, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test that duplicate entity names are NOT allowed on different devices.""" |     """Test that duplicate entity names are allowed on different devices.""" | ||||||
|     async with run_compiled(yaml_config), api_client_connected() as client: |     async with run_compiled(yaml_config), api_client_connected() as client: | ||||||
|         # Get device info |         # Get device info | ||||||
|         device_info = await client.device_info() |         device_info = await client.device_info() | ||||||
| @@ -52,46 +52,42 @@ async def test_duplicate_entities_not_allowed_on_different_devices( | |||||||
|         switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] |         switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] | ||||||
|         buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] |         buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] | ||||||
|         numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] |         numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] | ||||||
|         selects = [e for e in all_entities if e.__class__.__name__ == "SelectInfo"] |  | ||||||
|  |  | ||||||
|         # Scenario 1: Check that temperature sensors have unique names per device |         # Scenario 1: Check sensors with same "Temperature" name on different devices | ||||||
|         temp_sensors = [s for s in sensors if "Temperature" in s.name] |         temp_sensors = [s for s in sensors if s.name == "Temperature"] | ||||||
|         assert len(temp_sensors) == 4, ( |         assert len(temp_sensors) == 4, ( | ||||||
|             f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" |             f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Verify each sensor has a unique name |         # Verify each sensor is on a different device | ||||||
|         temp_names = set() |         temp_device_ids = set() | ||||||
|         temp_object_ids = set() |         temp_object_ids = set() | ||||||
|  |  | ||||||
|         for sensor in temp_sensors: |         for sensor in temp_sensors: | ||||||
|             temp_names.add(sensor.name) |             temp_device_ids.add(sensor.device_id) | ||||||
|             temp_object_ids.add(sensor.object_id) |             temp_object_ids.add(sensor.object_id) | ||||||
|  |  | ||||||
|         # Should have 4 unique names |             # All should have object_id "temperature" (no suffix) | ||||||
|         assert len(temp_names) == 4, ( |             assert sensor.object_id == "temperature", ( | ||||||
|             f"Temperature sensors should have unique names, got {temp_names}" |                 f"Expected object_id 'temperature', got '{sensor.object_id}'" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Should have 4 different device IDs (including None for main device) | ||||||
|  |         assert len(temp_device_ids) == 4, ( | ||||||
|  |             f"Temperature sensors should be on different devices, got {temp_device_ids}" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Object IDs should also be unique |         # Scenario 2: Check binary sensors "Status" on different devices | ||||||
|         assert len(temp_object_ids) == 4, ( |         status_binary = [b for b in binary_sensors if b.name == "Status"] | ||||||
|             f"Temperature sensors should have unique object_ids, got {temp_object_ids}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Scenario 2: Check binary sensors have unique names |  | ||||||
|         status_binary = [b for b in binary_sensors if "Status" in b.name] |  | ||||||
|         assert len(status_binary) == 3, ( |         assert len(status_binary) == 3, ( | ||||||
|             f"Expected exactly 3 status binary sensors, got {len(status_binary)}" |             f"Expected exactly 3 status binary sensors, got {len(status_binary)}" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # All should have unique object_ids |         # All should have object_id "status" | ||||||
|         status_names = set() |  | ||||||
|         for binary in status_binary: |         for binary in status_binary: | ||||||
|             status_names.add(binary.name) |             assert binary.object_id == "status", ( | ||||||
|  |                 f"Expected object_id 'status', got '{binary.object_id}'" | ||||||
|         assert len(status_names) == 3, ( |             ) | ||||||
|             f"Status binary sensors should have unique names, got {status_names}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Scenario 3: Check that sensor and binary_sensor can have same name |         # Scenario 3: Check that sensor and binary_sensor can have same name | ||||||
|         temp_binary = [b for b in binary_sensors if b.name == "Temperature"] |         temp_binary = [b for b in binary_sensors if b.name == "Temperature"] | ||||||
| @@ -100,86 +96,62 @@ async def test_duplicate_entities_not_allowed_on_different_devices( | |||||||
|         ) |         ) | ||||||
|         assert temp_binary[0].object_id == "temperature" |         assert temp_binary[0].object_id == "temperature" | ||||||
|  |  | ||||||
|         # Scenario 4: Check text sensors have unique names |         # Scenario 4: Check text sensors "Device Info" on different devices | ||||||
|         info_text = [t for t in text_sensors if "Device Info" in t.name] |         info_text = [t for t in text_sensors if t.name == "Device Info"] | ||||||
|         assert len(info_text) == 3, ( |         assert len(info_text) == 3, ( | ||||||
|             f"Expected exactly 3 device info text sensors, got {len(info_text)}" |             f"Expected exactly 3 device info text sensors, got {len(info_text)}" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # All should have unique names and object_ids |         # All should have object_id "device_info" | ||||||
|         info_names = set() |  | ||||||
|         for text in info_text: |         for text in info_text: | ||||||
|             info_names.add(text.name) |             assert text.object_id == "device_info", ( | ||||||
|  |                 f"Expected object_id 'device_info', got '{text.object_id}'" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         assert len(info_names) == 3, ( |         # Scenario 5: Check switches "Power" on different devices | ||||||
|             f"Device info text sensors should have unique names, got {info_names}" |         power_switches = [s for s in switches if s.name == "Power"] | ||||||
|  |         assert len(power_switches) == 3, ( | ||||||
|  |             f"Expected exactly 3 power switches, got {len(power_switches)}" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Scenario 5: Check switches have unique names |         # All should have object_id "power" | ||||||
|         power_switches = [s for s in switches if "Power" in s.name] |  | ||||||
|         assert len(power_switches) == 4, ( |  | ||||||
|             f"Expected exactly 4 power switches, got {len(power_switches)}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # All should have unique names |  | ||||||
|         power_names = set() |  | ||||||
|         for switch in power_switches: |         for switch in power_switches: | ||||||
|             power_names.add(switch.name) |             assert switch.object_id == "power", ( | ||||||
|  |                 f"Expected object_id 'power', got '{switch.object_id}'" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         assert len(power_names) == 4, ( |         # Scenario 6: Check empty name buttons (should use device name) | ||||||
|             f"Power switches should have unique names, got {power_names}" |         empty_buttons = [b for b in buttons if b.name == ""] | ||||||
|         ) |         assert len(empty_buttons) == 3, ( | ||||||
|  |             f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}" | ||||||
|         # Scenario 6: Check reset buttons have unique names |  | ||||||
|         reset_buttons = [b for b in buttons if "Reset" in b.name] |  | ||||||
|         assert len(reset_buttons) == 3, ( |  | ||||||
|             f"Expected exactly 3 reset buttons, got {len(reset_buttons)}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # All should have unique names |  | ||||||
|         reset_names = set() |  | ||||||
|         for button in reset_buttons: |  | ||||||
|             reset_names.add(button.name) |  | ||||||
|  |  | ||||||
|         assert len(reset_names) == 3, ( |  | ||||||
|             f"Reset buttons should have unique names, got {reset_names}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Scenario 7: Check empty name selects (should use device names) |  | ||||||
|         empty_selects = [s for s in selects if s.name == ""] |  | ||||||
|         assert len(empty_selects) == 3, ( |  | ||||||
|             f"Expected exactly 3 empty name selects, got {len(empty_selects)}" |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Group by device |         # Group by device | ||||||
|         c1_selects = [s for s in empty_selects if s.device_id == controller_1.device_id] |         c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id] | ||||||
|         c2_selects = [s for s in empty_selects if s.device_id == controller_2.device_id] |         c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id] | ||||||
|  |  | ||||||
|         # For main device, device_id is 0 |         # For main device, device_id is 0 | ||||||
|         main_selects = [s for s in empty_selects if s.device_id == 0] |         main_buttons = [b for b in empty_buttons if b.device_id == 0] | ||||||
|  |  | ||||||
|         # Check object IDs for empty name entities - they should use device names |         # Check object IDs for empty name entities | ||||||
|         assert len(c1_selects) == 1 and c1_selects[0].object_id == "controller_1" |         assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1" | ||||||
|         assert len(c2_selects) == 1 and c2_selects[0].object_id == "controller_2" |         assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2" | ||||||
|         assert ( |         assert ( | ||||||
|             len(main_selects) == 1 |             len(main_buttons) == 1 | ||||||
|             and main_selects[0].object_id == "duplicate-entities-test" |             and main_buttons[0].object_id == "duplicate-entities-test" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Scenario 8: Check special characters in number names - now unique |         # Scenario 7: Check special characters in number names | ||||||
|         temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name] |         temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"] | ||||||
|         assert len(temp_numbers) == 2, ( |         assert len(temp_numbers) == 2, ( | ||||||
|             f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" |             f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Should have unique names |         # Special characters should be sanitized to _ in object_id | ||||||
|         setpoint_names = set() |  | ||||||
|         for number in temp_numbers: |         for number in temp_numbers: | ||||||
|             setpoint_names.add(number.name) |             assert number.object_id == "temperature_setpoint_", ( | ||||||
|  |                 f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'" | ||||||
|         assert len(setpoint_names) == 2, ( |             ) | ||||||
|             f"Temperature setpoint numbers should have unique names, got {setpoint_names}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Verify we can get states for all entities (ensures they're functional) |         # Verify we can get states for all entities (ensures they're functional) | ||||||
|         loop = asyncio.get_running_loop() |         loop = asyncio.get_running_loop() | ||||||
| @@ -192,7 +164,6 @@ async def test_duplicate_entities_not_allowed_on_different_devices( | |||||||
|             + len(switches) |             + len(switches) | ||||||
|             + len(buttons) |             + len(buttons) | ||||||
|             + len(numbers) |             + len(numbers) | ||||||
|             + len(selects) |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         def on_state(state) -> None: |         def on_state(state) -> None: | ||||||
|   | |||||||
| @@ -986,8 +986,7 @@ def test_get_components_from_integration_fixtures() -> None: | |||||||
|  |  | ||||||
|     with ( |     with ( | ||||||
|         patch("pathlib.Path.glob") as mock_glob, |         patch("pathlib.Path.glob") as mock_glob, | ||||||
|         patch("builtins.open", create=True), |         patch("esphome.yaml_util.load_yaml", return_value=yaml_content), | ||||||
|         patch("yaml.safe_load", return_value=yaml_content), |  | ||||||
|     ): |     ): | ||||||
|         mock_glob.return_value = [mock_yaml_file] |         mock_glob.return_value = [mock_yaml_file] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -510,13 +510,13 @@ def test_entity_duplicate_validator() -> None: | |||||||
|     config1 = {CONF_NAME: "Temperature"} |     config1 = {CONF_NAME: "Temperature"} | ||||||
|     validated1 = validator(config1) |     validated1 = validator(config1) | ||||||
|     assert validated1 == config1 |     assert validated1 == config1 | ||||||
|     assert ("sensor", "temperature") in CORE.unique_ids |     assert ("", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |  | ||||||
|     # Second entity with different name should pass |     # Second entity with different name should pass | ||||||
|     config2 = {CONF_NAME: "Humidity"} |     config2 = {CONF_NAME: "Humidity"} | ||||||
|     validated2 = validator(config2) |     validated2 = validator(config2) | ||||||
|     assert validated2 == config2 |     assert validated2 == config2 | ||||||
|     assert ("sensor", "humidity") in CORE.unique_ids |     assert ("", "sensor", "humidity") in CORE.unique_ids | ||||||
|  |  | ||||||
|     # Duplicate entity should fail |     # Duplicate entity should fail | ||||||
|     config3 = {CONF_NAME: "Temperature"} |     config3 = {CONF_NAME: "Temperature"} | ||||||
| @@ -535,36 +535,24 @@ def test_entity_duplicate_validator_with_devices() -> None: | |||||||
|     device1 = ID("device1", type="Device") |     device1 = ID("device1", type="Device") | ||||||
|     device2 = ID("device2", type="Device") |     device2 = ID("device2", type="Device") | ||||||
|  |  | ||||||
|     # First entity on device1 should pass |     # Same name on different devices should pass | ||||||
|     config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} |     config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} | ||||||
|     validated1 = validator(config1) |     validated1 = validator(config1) | ||||||
|     assert validated1 == config1 |     assert validated1 == config1 | ||||||
|     assert ("sensor", "temperature") in CORE.unique_ids |     assert ("device1", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |  | ||||||
|     # Same name on different device should now fail |  | ||||||
|     config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} |     config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} | ||||||
|  |     validated2 = validator(config2) | ||||||
|  |     assert validated2 == config2 | ||||||
|  |     assert ("device2", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |  | ||||||
|  |     # Duplicate on same device should fail | ||||||
|  |     config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} | ||||||
|     with pytest.raises( |     with pytest.raises( | ||||||
|         Invalid, |         Invalid, | ||||||
|         match=r"Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices.", |         match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", | ||||||
|     ): |     ): | ||||||
|         validator(config2) |         validator(config3) | ||||||
|  |  | ||||||
|     # Different name on device2 should pass |  | ||||||
|     config3 = {CONF_NAME: "Humidity", CONF_DEVICE_ID: device2} |  | ||||||
|     validated3 = validator(config3) |  | ||||||
|     assert validated3 == config3 |  | ||||||
|     assert ("sensor", "humidity") in CORE.unique_ids |  | ||||||
|  |  | ||||||
|     # Empty names should use device names and be allowed |  | ||||||
|     config4 = {CONF_NAME: "", CONF_DEVICE_ID: device1} |  | ||||||
|     validated4 = validator(config4) |  | ||||||
|     assert validated4 == config4 |  | ||||||
|     assert ("sensor", "device1") in CORE.unique_ids |  | ||||||
|  |  | ||||||
|     config5 = {CONF_NAME: "", CONF_DEVICE_ID: device2} |  | ||||||
|     validated5 = validator(config5) |  | ||||||
|     assert validated5 == config5 |  | ||||||
|     assert ("sensor", "device2") in CORE.unique_ids |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_duplicate_entity_yaml_validation( | def test_duplicate_entity_yaml_validation( | ||||||
| @@ -588,10 +576,10 @@ def test_duplicate_entity_with_devices_yaml_validation( | |||||||
|     ) |     ) | ||||||
|     assert result is None |     assert result is None | ||||||
|  |  | ||||||
|     # Check for the duplicate entity error message |     # Check for the duplicate entity error message with device | ||||||
|     captured = capsys.readouterr() |     captured = capsys.readouterr() | ||||||
|     assert ( |     assert ( | ||||||
|         "Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices." |         "Duplicate sensor entity with name 'Temperature' found on device 'device1'" | ||||||
|         in captured.out |         in captured.out | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -616,21 +604,22 @@ def test_entity_duplicate_validator_internal_entities() -> None: | |||||||
|     config1 = {CONF_NAME: "Temperature"} |     config1 = {CONF_NAME: "Temperature"} | ||||||
|     validated1 = validator(config1) |     validated1 = validator(config1) | ||||||
|     assert validated1 == config1 |     assert validated1 == config1 | ||||||
|     assert ("sensor", "temperature") in CORE.unique_ids |     # New format includes device_id (empty string for main device) | ||||||
|  |     assert ("", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |  | ||||||
|     # Internal entity with same name should pass (not added to unique_ids) |     # Internal entity with same name should pass (not added to unique_ids) | ||||||
|     config2 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} |     config2 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} | ||||||
|     validated2 = validator(config2) |     validated2 = validator(config2) | ||||||
|     assert validated2 == config2 |     assert validated2 == config2 | ||||||
|     # Internal entity should not be added to unique_ids |     # Internal entity should not be added to unique_ids | ||||||
|     assert len([k for k in CORE.unique_ids if k == ("sensor", "temperature")]) == 1 |     assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 | ||||||
|  |  | ||||||
|     # Another internal entity with same name should also pass |     # Another internal entity with same name should also pass | ||||||
|     config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} |     config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} | ||||||
|     validated3 = validator(config3) |     validated3 = validator(config3) | ||||||
|     assert validated3 == config3 |     assert validated3 == config3 | ||||||
|     # Still only one entry in unique_ids (from the non-internal entity) |     # Still only one entry in unique_ids (from the non-internal entity) | ||||||
|     assert len([k for k in CORE.unique_ids if k == ("sensor", "temperature")]) == 1 |     assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 | ||||||
|  |  | ||||||
|     # Non-internal entity with same name should fail |     # Non-internal entity with same name should fail | ||||||
|     config4 = {CONF_NAME: "Temperature"} |     config4 = {CONF_NAME: "Temperature"} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user