mirror of
https://github.com/esphome/esphome.git
synced 2025-08-02 00:17:48 +00:00
Merge branch 'template_send_message' into integration
This commit is contained in:
commit
e1583ff2d3
@ -5,6 +5,7 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32S2,
|
||||
@ -85,6 +86,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
3: adc_channel_t.ADC_CHANNEL_3,
|
||||
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
|
||||
VARIANT_ESP32C6: {
|
||||
0: adc_channel_t.ADC_CHANNEL_0,
|
||||
@ -155,6 +166,8 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
VARIANT_ESP32C3: {
|
||||
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
|
||||
VARIANT_ESP32C6: {}, # no ADC2
|
||||
# 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).
|
||||
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.
|
||||
/// @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) {
|
||||
this->adc_unit_ = unit;
|
||||
this->channel_ = channel;
|
||||
|
@ -43,9 +43,10 @@ void ADCSensor::setup() {
|
||||
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
||||
init_config.unit_id = this->adc_unit_;
|
||||
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;
|
||||
#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_]);
|
||||
if (err != ESP_OK) {
|
||||
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;
|
||||
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
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#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);
|
||||
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;
|
||||
@ -185,11 +187,12 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
} else {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
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_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -217,7 +220,8 @@ float ADCSensor::sample_autorange_() {
|
||||
// Need to recalibrate for the new attenuation
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// 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_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@ -228,7 +232,8 @@ float ADCSensor::sample_autorange_() {
|
||||
// Create new calibration handle for this attenuation
|
||||
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 = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@ -256,7 +261,8 @@ float ADCSensor::sample_autorange_() {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
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);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
@ -275,7 +281,8 @@ float ADCSensor::sample_autorange_() {
|
||||
voltage = raw * 3.3f / 4095.0f;
|
||||
}
|
||||
// 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);
|
||||
#else
|
||||
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;
|
||||
#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) \
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
|
||||
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 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) \
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
|
||||
if ((entity_var) == nullptr) \
|
||||
return;
|
||||
#endif // USE_DEVICES
|
||||
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
: 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) {
|
||||
// Only send ping if we're not disconnecting
|
||||
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 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
|
||||
@ -232,7 +252,7 @@ void APIConnection::loop() {
|
||||
resp.entity_id = it.entity_id;
|
||||
resp.attribute = it.attribute.value();
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp)) {
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
state_subs_at_++;
|
||||
}
|
||||
} 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.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) {
|
||||
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) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
return;
|
||||
this->send_message(call);
|
||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
@ -133,7 +133,7 @@ class APIConnection : public APIServerConnection {
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void send_time_request() {
|
||||
GetTimeRequest req;
|
||||
this->send_message(req);
|
||||
this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
|
||||
}
|
||||
#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) {
|
||||
HelloResponse ret = this->hello(msg);
|
||||
if (!this->send_message(ret)) {
|
||||
if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
|
||||
ConnectResponse ret = this->connect(msg);
|
||||
if (!this->send_message(ret)) {
|
||||
if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
|
||||
DisconnectResponse ret = this->disconnect(msg);
|
||||
if (!this->send_message(ret)) {
|
||||
if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_ping_request(const PingRequest &msg) {
|
||||
PingResponse ret = this->ping(msg);
|
||||
if (!this->send_message(ret)) {
|
||||
if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
|
||||
if (this->check_connection_setup_()) {
|
||||
DeviceInfoResponse ret = this->device_info(msg);
|
||||
if (!this->send_message(ret)) {
|
||||
if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) {
|
||||
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) {
|
||||
if (this->check_connection_setup_()) {
|
||||
GetTimeResponse ret = this->get_time(msg);
|
||||
if (!this->send_message(ret)) {
|
||||
if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) {
|
||||
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) {
|
||||
if (this->check_authenticated_()) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -867,7 +867,7 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -899,7 +899,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
|
||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService {
|
||||
public:
|
||||
#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
|
||||
this->log_send_message_(msg.message_name(), msg.dump());
|
||||
#endif
|
||||
return this->send_message_(msg, T::MESSAGE_TYPE);
|
||||
return this->send_message_(msg, message_type);
|
||||
}
|
||||
|
||||
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");
|
||||
this->set_noise_psk(psk);
|
||||
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
|
||||
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),
|
||||
// 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,
|
||||
|
@ -86,7 +86,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
|
||||
#ifdef USE_API_SERVICES
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp);
|
||||
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#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);
|
||||
// Use bulk insert instead of individual push_backs
|
||||
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;
|
||||
}
|
||||
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;
|
||||
resp.address = this->address_;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
resp.address = this->address_;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
resp.address = this->address_;
|
||||
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;
|
||||
}
|
||||
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);
|
||||
// Use bulk insert instead of individual push_backs
|
||||
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;
|
||||
}
|
||||
default:
|
||||
|
@ -39,7 +39,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
|
||||
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
|
||||
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
: 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
|
||||
@ -111,7 +111,7 @@ void BluetoothProxy::flush_pending_advertisements() {
|
||||
|
||||
api::BluetoothLERawAdvertisementsResponse resp;
|
||||
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
|
||||
@ -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());
|
||||
}
|
||||
|
||||
this->api_connection_->send_message(resp);
|
||||
this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
@ -309,7 +309,7 @@ void BluetoothProxy::loop() {
|
||||
service_resp.characteristics.push_back(std::move(characteristic_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.error = ret;
|
||||
|
||||
this->api_connection_->send_message(call);
|
||||
this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -582,7 +582,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
|
||||
call.connected = connected;
|
||||
call.mtu = mtu;
|
||||
call.error = error;
|
||||
this->api_connection_->send_message(call);
|
||||
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void BluetoothProxy::send_connections_free() {
|
||||
if (this->api_connection_ == nullptr)
|
||||
@ -595,7 +595,7 @@ void BluetoothProxy::send_connections_free() {
|
||||
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) {
|
||||
@ -603,7 +603,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||
return;
|
||||
api::BluetoothGATTGetServicesDoneResponse call;
|
||||
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) {
|
||||
@ -613,7 +613,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_
|
||||
call.address = address;
|
||||
call.handle = handle;
|
||||
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) {
|
||||
@ -622,7 +622,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_
|
||||
call.paired = paired;
|
||||
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) {
|
||||
@ -631,7 +631,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e
|
||||
call.success = success;
|
||||
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) {
|
||||
|
@ -19,6 +19,7 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_AREA,
|
||||
@ -81,6 +82,7 @@ from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_AREA,
|
||||
|
@ -41,6 +41,7 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WINDOW_SIZE,
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_AREA,
|
||||
@ -107,6 +108,7 @@ from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_AREA,
|
||||
|
@ -223,7 +223,8 @@ void VoiceAssistant::loop() {
|
||||
msg.wake_word_phrase = 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");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start");
|
||||
this->continuous_ = false;
|
||||
@ -245,7 +246,7 @@ void VoiceAssistant::loop() {
|
||||
if (this->audio_mode_ == AUDIO_MODE_API) {
|
||||
api::VoiceAssistantAudio msg;
|
||||
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 {
|
||||
if (!this->udp_socket_running_) {
|
||||
if (!this->start_udp_socket_()) {
|
||||
@ -331,7 +332,7 @@ void VoiceAssistant::loop() {
|
||||
|
||||
api::VoiceAssistantAnnounceFinished msg;
|
||||
msg.success = true;
|
||||
this->api_client_->send_message(msg);
|
||||
this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -580,7 +581,7 @@ void VoiceAssistant::signal_stop_() {
|
||||
ESP_LOGD(TAG, "Signaling stop");
|
||||
api::VoiceAssistantRequest msg;
|
||||
msg.start = false;
|
||||
this->api_client_->send_message(msg);
|
||||
this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void VoiceAssistant::start_playback_timeout_() {
|
||||
@ -590,7 +591,7 @@ void VoiceAssistant::start_playback_timeout_() {
|
||||
|
||||
api::VoiceAssistantAnnounceFinished msg;
|
||||
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"
|
||||
|
||||
# device classes
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY = "absolute_humidity"
|
||||
DEVICE_CLASS_APPARENT_POWER = "apparent_power"
|
||||
DEVICE_CLASS_AQI = "aqi"
|
||||
DEVICE_CLASS_AREA = "area"
|
||||
|
@ -368,8 +368,19 @@ class Application {
|
||||
|
||||
uint8_t get_app_state() const { return this->app_state_; }
|
||||
|
||||
// Helper macro for entity getter method declarations - reduces code duplication
|
||||
// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter
|
||||
// Helper macro for entity getter method declarations
|
||||
#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) \
|
||||
entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
|
||||
for (auto *obj : this->entities_member##_) { \
|
||||
@ -378,10 +389,7 @@ class Application {
|
||||
} \
|
||||
return nullptr; \
|
||||
}
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
const std::vector<Device *> &get_devices() { return this->devices_; }
|
||||
#endif
|
||||
#endif // USE_DEVICES
|
||||
#ifdef USE_AREAS
|
||||
const std::vector<Area *> &get_areas() { return this->areas_; }
|
||||
#endif
|
||||
|
@ -198,9 +198,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
||||
|
||||
# Get device name if entity is on a sub-device
|
||||
device_name = None
|
||||
device_id = "" # Empty string for main device
|
||||
if CONF_DEVICE_ID in config:
|
||||
device_id_obj = config[CONF_DEVICE_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
|
||||
# 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
|
||||
unique_key = (platform, name_key)
|
||||
unique_key = (device_id, platform, name_key)
|
||||
if unique_key in CORE.unique_ids:
|
||||
device_prefix = f" on device '{device_id}'" if device_id else ""
|
||||
raise cv.Invalid(
|
||||
f"Duplicate {platform} entity with name '{entity_name}' found. "
|
||||
f"Each entity must have a unique name within its platform across all devices."
|
||||
f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
|
||||
f"Each entity on a device must have a unique name within its platform."
|
||||
)
|
||||
|
||||
# Add to tracking set
|
||||
|
@ -1713,13 +1713,12 @@ static const char *const TAG = "api.service";
|
||||
hpp += " public:\n"
|
||||
hpp += "#endif\n\n"
|
||||
|
||||
# Add generic send_message method
|
||||
hpp += " template<typename T>\n"
|
||||
hpp += " bool send_message(const T &msg) {\n"
|
||||
# Add non-template send_message method
|
||||
hpp += " bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n"
|
||||
hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
|
||||
hpp += " this->log_send_message_(msg.message_name(), msg.dump());\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"
|
||||
|
||||
# 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"
|
||||
else:
|
||||
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 += "}\n"
|
||||
|
||||
@ -1818,7 +1819,7 @@ static const char *const TAG = "api.service";
|
||||
body += f"this->{func}(msg);\n"
|
||||
else:
|
||||
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 += "}\n"
|
||||
|
||||
|
@ -530,27 +530,26 @@ def get_components_from_integration_fixtures() -> set[str]:
|
||||
Returns:
|
||||
Set of component names used in integration test fixtures
|
||||
"""
|
||||
import yaml
|
||||
from esphome import yaml_util
|
||||
|
||||
components: set[str] = set()
|
||||
fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures"
|
||||
|
||||
for yaml_file in fixtures_dir.glob("*.yaml"):
|
||||
with open(yaml_file) as f:
|
||||
config: dict[str, any] | None = yaml.safe_load(f)
|
||||
if not config:
|
||||
config: dict[str, any] | None = yaml_util.load_yaml(str(yaml_file))
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
for item in value:
|
||||
if isinstance(item, dict) and "platform" in item:
|
||||
components.add(item["platform"])
|
||||
for item in value:
|
||||
if isinstance(item, dict) and "platform" in item:
|
||||
components.add(item["platform"])
|
||||
|
||||
return components
|
||||
|
@ -54,3 +54,45 @@ sensor:
|
||||
device_id: smart_switch_device
|
||||
lambda: return 4.0;
|
||||
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:
|
||||
name: duplicate-entities-test
|
||||
# Define devices to test multi-device unique name validation
|
||||
# Define devices to test multi-device duplicate handling
|
||||
devices:
|
||||
- id: controller_1
|
||||
name: Controller 1
|
||||
@ -13,31 +13,31 @@ host:
|
||||
api: # Port will be automatically injected
|
||||
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:
|
||||
- platform: template
|
||||
name: Temperature Controller 1
|
||||
name: Temperature
|
||||
device_id: controller_1
|
||||
lambda: return 21.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
- platform: template
|
||||
name: Temperature Controller 2
|
||||
name: Temperature
|
||||
device_id: controller_2
|
||||
lambda: return 22.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
- platform: template
|
||||
name: Temperature Controller 3
|
||||
name: Temperature
|
||||
device_id: controller_3
|
||||
lambda: return 23.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
# Main device sensor (no device_id)
|
||||
- platform: template
|
||||
name: Temperature Main
|
||||
name: Temperature
|
||||
lambda: return 20.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
@ -47,20 +47,20 @@ sensor:
|
||||
lambda: return 60.0;
|
||||
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:
|
||||
- platform: template
|
||||
name: Status Controller 1
|
||||
name: Status
|
||||
device_id: controller_1
|
||||
lambda: return true;
|
||||
|
||||
- platform: template
|
||||
name: Status Controller 2
|
||||
name: Status
|
||||
device_id: controller_2
|
||||
lambda: return false;
|
||||
|
||||
- platform: template
|
||||
name: Status Main
|
||||
name: Status
|
||||
lambda: return true; # Main device
|
||||
|
||||
# Different platform can have same name as sensor
|
||||
@ -68,43 +68,43 @@ binary_sensor:
|
||||
name: Temperature
|
||||
lambda: return true;
|
||||
|
||||
# Scenario 3: Different text sensor names on different devices
|
||||
# Scenario 3: Same text sensor name on different devices
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: Device Info Controller 1
|
||||
name: Device Info
|
||||
device_id: controller_1
|
||||
lambda: return {"Controller 1 Active"};
|
||||
update_interval: 0.1s
|
||||
|
||||
- platform: template
|
||||
name: Device Info Controller 2
|
||||
name: Device Info
|
||||
device_id: controller_2
|
||||
lambda: return {"Controller 2 Active"};
|
||||
update_interval: 0.1s
|
||||
|
||||
- platform: template
|
||||
name: Device Info Main
|
||||
name: Device Info
|
||||
lambda: return {"Main Device Active"};
|
||||
update_interval: 0.1s
|
||||
|
||||
# Scenario 4: Different switch names on different devices
|
||||
# Scenario 4: Same switch name on different devices
|
||||
switch:
|
||||
- platform: template
|
||||
name: Power Controller 1
|
||||
name: Power
|
||||
device_id: controller_1
|
||||
lambda: return false;
|
||||
turn_on_action: []
|
||||
turn_off_action: []
|
||||
|
||||
- platform: template
|
||||
name: Power Controller 2
|
||||
name: Power
|
||||
device_id: controller_2
|
||||
lambda: return true;
|
||||
turn_on_action: []
|
||||
turn_off_action: []
|
||||
|
||||
- platform: template
|
||||
name: Power Controller 3
|
||||
name: Power
|
||||
device_id: controller_3
|
||||
lambda: return false;
|
||||
turn_on_action: []
|
||||
@ -117,54 +117,26 @@ switch:
|
||||
turn_on_action: []
|
||||
turn_off_action: []
|
||||
|
||||
# Scenario 5: Buttons with unique names
|
||||
# Scenario 5: Empty names on different devices (should use device name)
|
||||
button:
|
||||
- platform: template
|
||||
name: "Reset Controller 1"
|
||||
name: ""
|
||||
device_id: controller_1
|
||||
on_press: []
|
||||
|
||||
- platform: template
|
||||
name: "Reset Controller 2"
|
||||
name: ""
|
||||
device_id: controller_2
|
||||
on_press: []
|
||||
|
||||
- platform: template
|
||||
name: "Reset Main"
|
||||
name: ""
|
||||
on_press: [] # Main device
|
||||
|
||||
# Scenario 6: Empty names (should use device 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
|
||||
# Scenario 6: Special characters in names
|
||||
number:
|
||||
- platform: template
|
||||
name: "Temperature Setpoint! Controller 1"
|
||||
name: "Temperature Setpoint!"
|
||||
device_id: controller_1
|
||||
min_value: 10.0
|
||||
max_value: 30.0
|
||||
@ -173,7 +145,7 @@ number:
|
||||
set_action: []
|
||||
|
||||
- platform: template
|
||||
name: "Temperature Setpoint! Controller 2"
|
||||
name: "Temperature Setpoint!"
|
||||
device_id: controller_2
|
||||
min_value: 10.0
|
||||
max_value: 30.0
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState
|
||||
from aioesphomeapi import EntityState, SwitchInfo, SwitchState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@ -84,23 +84,45 @@ async def test_areas_and_devices(
|
||||
|
||||
# Subscribe to states to get sensor values
|
||||
loop = asyncio.get_running_loop()
|
||||
states: dict[int, EntityState] = {}
|
||||
states_future: asyncio.Future[bool] = loop.create_future()
|
||||
states: dict[tuple[int, int], EntityState] = {}
|
||||
# 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:
|
||||
states[state.key] = state
|
||||
# Check if we have all expected sensor states
|
||||
if len(states) >= 4 and not states_future.done():
|
||||
states_future.set_result(True)
|
||||
state_key = (state.device_id, state.key)
|
||||
states[state_key] = state
|
||||
|
||||
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)
|
||||
|
||||
# Wait for sensor states
|
||||
try:
|
||||
await asyncio.wait_for(states_future, timeout=10.0)
|
||||
await asyncio.wait_for(initial_states_future, timeout=10.0)
|
||||
except TimeoutError:
|
||||
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"
|
||||
)
|
||||
|
||||
@ -119,3 +141,121 @@ async def test_areas_and_devices(
|
||||
f"{entity.name} has device_id {entity.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
|
||||
async def test_duplicate_entities_not_allowed_on_different_devices(
|
||||
async def test_duplicate_entities_on_different_devices(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> 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:
|
||||
# Get 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"]
|
||||
buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"]
|
||||
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
|
||||
temp_sensors = [s for s in sensors if "Temperature" in s.name]
|
||||
# Scenario 1: Check sensors with same "Temperature" name on different devices
|
||||
temp_sensors = [s for s in sensors if s.name == "Temperature"]
|
||||
assert len(temp_sensors) == 4, (
|
||||
f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}"
|
||||
)
|
||||
|
||||
# Verify each sensor has a unique name
|
||||
temp_names = set()
|
||||
# Verify each sensor is on a different device
|
||||
temp_device_ids = set()
|
||||
temp_object_ids = set()
|
||||
|
||||
for sensor in temp_sensors:
|
||||
temp_names.add(sensor.name)
|
||||
temp_device_ids.add(sensor.device_id)
|
||||
temp_object_ids.add(sensor.object_id)
|
||||
|
||||
# Should have 4 unique names
|
||||
assert len(temp_names) == 4, (
|
||||
f"Temperature sensors should have unique names, got {temp_names}"
|
||||
# All should have object_id "temperature" (no suffix)
|
||||
assert sensor.object_id == "temperature", (
|
||||
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
|
||||
assert len(temp_object_ids) == 4, (
|
||||
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]
|
||||
# Scenario 2: Check binary sensors "Status" on different devices
|
||||
status_binary = [b for b in binary_sensors if b.name == "Status"]
|
||||
assert len(status_binary) == 3, (
|
||||
f"Expected exactly 3 status binary sensors, got {len(status_binary)}"
|
||||
)
|
||||
|
||||
# All should have unique object_ids
|
||||
status_names = set()
|
||||
# All should have object_id "status"
|
||||
for binary in status_binary:
|
||||
status_names.add(binary.name)
|
||||
|
||||
assert len(status_names) == 3, (
|
||||
f"Status binary sensors should have unique names, got {status_names}"
|
||||
)
|
||||
assert binary.object_id == "status", (
|
||||
f"Expected object_id 'status', got '{binary.object_id}'"
|
||||
)
|
||||
|
||||
# Scenario 3: Check that sensor and binary_sensor can have same name
|
||||
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"
|
||||
|
||||
# Scenario 4: Check text sensors have unique names
|
||||
info_text = [t for t in text_sensors if "Device Info" in t.name]
|
||||
# Scenario 4: Check text sensors "Device Info" on different devices
|
||||
info_text = [t for t in text_sensors if t.name == "Device Info"]
|
||||
assert len(info_text) == 3, (
|
||||
f"Expected exactly 3 device info text sensors, got {len(info_text)}"
|
||||
)
|
||||
|
||||
# All should have unique names and object_ids
|
||||
info_names = set()
|
||||
# All should have object_id "device_info"
|
||||
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, (
|
||||
f"Device info text sensors should have unique names, got {info_names}"
|
||||
# Scenario 5: Check switches "Power" on different devices
|
||||
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
|
||||
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()
|
||||
# All should have object_id "power"
|
||||
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, (
|
||||
f"Power switches should have unique names, got {power_names}"
|
||||
)
|
||||
|
||||
# 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)}"
|
||||
# Scenario 6: Check empty name buttons (should use device name)
|
||||
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)}"
|
||||
)
|
||||
|
||||
# Group by device
|
||||
c1_selects = [s for s in empty_selects if s.device_id == controller_1.device_id]
|
||||
c2_selects = [s for s in empty_selects if s.device_id == controller_2.device_id]
|
||||
c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.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
|
||||
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
|
||||
assert len(c1_selects) == 1 and c1_selects[0].object_id == "controller_1"
|
||||
assert len(c2_selects) == 1 and c2_selects[0].object_id == "controller_2"
|
||||
# Check object IDs for empty name entities
|
||||
assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1"
|
||||
assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2"
|
||||
assert (
|
||||
len(main_selects) == 1
|
||||
and main_selects[0].object_id == "duplicate-entities-test"
|
||||
len(main_buttons) == 1
|
||||
and main_buttons[0].object_id == "duplicate-entities-test"
|
||||
)
|
||||
|
||||
# Scenario 8: Check special characters in number names - now unique
|
||||
temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name]
|
||||
# Scenario 7: Check special characters in number names
|
||||
temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"]
|
||||
assert len(temp_numbers) == 2, (
|
||||
f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}"
|
||||
)
|
||||
|
||||
# Should have unique names
|
||||
setpoint_names = set()
|
||||
# Special characters should be sanitized to _ in object_id
|
||||
for number in temp_numbers:
|
||||
setpoint_names.add(number.name)
|
||||
|
||||
assert len(setpoint_names) == 2, (
|
||||
f"Temperature setpoint numbers should have unique names, got {setpoint_names}"
|
||||
)
|
||||
assert number.object_id == "temperature_setpoint_", (
|
||||
f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'"
|
||||
)
|
||||
|
||||
# Verify we can get states for all entities (ensures they're functional)
|
||||
loop = asyncio.get_running_loop()
|
||||
@ -192,7 +164,6 @@ async def test_duplicate_entities_not_allowed_on_different_devices(
|
||||
+ len(switches)
|
||||
+ len(buttons)
|
||||
+ len(numbers)
|
||||
+ len(selects)
|
||||
)
|
||||
|
||||
def on_state(state) -> None:
|
||||
|
@ -986,8 +986,7 @@ def test_get_components_from_integration_fixtures() -> None:
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.glob") as mock_glob,
|
||||
patch("builtins.open", create=True),
|
||||
patch("yaml.safe_load", return_value=yaml_content),
|
||||
patch("esphome.yaml_util.load_yaml", return_value=yaml_content),
|
||||
):
|
||||
mock_glob.return_value = [mock_yaml_file]
|
||||
|
||||
|
@ -510,13 +510,13 @@ def test_entity_duplicate_validator() -> None:
|
||||
config1 = {CONF_NAME: "Temperature"}
|
||||
validated1 = validator(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
|
||||
config2 = {CONF_NAME: "Humidity"}
|
||||
validated2 = validator(config2)
|
||||
assert validated2 == config2
|
||||
assert ("sensor", "humidity") in CORE.unique_ids
|
||||
assert ("", "sensor", "humidity") in CORE.unique_ids
|
||||
|
||||
# Duplicate entity should fail
|
||||
config3 = {CONF_NAME: "Temperature"}
|
||||
@ -535,36 +535,24 @@ def test_entity_duplicate_validator_with_devices() -> None:
|
||||
device1 = ID("device1", 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}
|
||||
validated1 = validator(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}
|
||||
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(
|
||||
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)
|
||||
|
||||
# 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
|
||||
validator(config3)
|
||||
|
||||
|
||||
def test_duplicate_entity_yaml_validation(
|
||||
@ -588,10 +576,10 @@ def test_duplicate_entity_with_devices_yaml_validation(
|
||||
)
|
||||
assert result is None
|
||||
|
||||
# Check for the duplicate entity error message
|
||||
# Check for the duplicate entity error message with device
|
||||
captured = capsys.readouterr()
|
||||
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
|
||||
)
|
||||
|
||||
@ -616,21 +604,22 @@ def test_entity_duplicate_validator_internal_entities() -> None:
|
||||
config1 = {CONF_NAME: "Temperature"}
|
||||
validated1 = validator(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)
|
||||
config2 = {CONF_NAME: "Temperature", CONF_INTERNAL: True}
|
||||
validated2 = validator(config2)
|
||||
assert validated2 == config2
|
||||
# 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
|
||||
config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True}
|
||||
validated3 = validator(config3)
|
||||
assert validated3 == config3
|
||||
# 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
|
||||
config4 = {CONF_NAME: "Temperature"}
|
||||
|
Loading…
x
Reference in New Issue
Block a user