Merge branch 'template_send_message' into integration

This commit is contained in:
J. Nick Koston 2025-07-16 08:26:29 -10:00
commit e1583ff2d3
No known key found for this signature in database
25 changed files with 435 additions and 261 deletions

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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();
} }
} }

View File

@ -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){};

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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) {

View File

@ -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,

View File

@ -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,

View File

@ -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);
}); });
} }

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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:

View File

@ -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]

View 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"}