mirror of
https://github.com/esphome/esphome.git
synced 2026-05-24 18:06:27 +08:00
[core] Move device class strings to PROGMEM on ESP8266 (#14443)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -396,6 +396,48 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
|
||||
return static_cast<uint16_t>(header_padding + calculated_size + footer_size);
|
||||
}
|
||||
|
||||
uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
|
||||
// API 1.14+ clients compute object_id client-side from the entity name
|
||||
// For older clients, we must send object_id for backward compatibility
|
||||
// See: https://github.com/esphome/backlog/issues/76
|
||||
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
|
||||
// Buffer must remain in scope until encode_message_to_buffer is called
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
if (!conn->client_supports_api_version(1, 14)) {
|
||||
msg.object_id = entity->get_object_id_to(object_id_buf);
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.name = entity->get_name();
|
||||
}
|
||||
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
char icon_buf[MAX_ICON_LENGTH];
|
||||
msg.icon = StringRef(entity->get_icon_to(icon_buf));
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
|
||||
}
|
||||
|
||||
uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
StringRef &device_class_field,
|
||||
uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
char dc_buf[MAX_DEVICE_CLASS_LENGTH];
|
||||
device_class_field = StringRef(entity->get_device_class_to(dc_buf));
|
||||
return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size);
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
|
||||
return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE,
|
||||
@@ -414,10 +456,9 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
|
||||
uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
|
||||
ListEntitiesBinarySensorResponse msg;
|
||||
msg.device_class = binary_sensor->get_device_class_ref();
|
||||
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
|
||||
return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(
|
||||
binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -443,8 +484,8 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_tilt = traits.get_supports_tilt();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
msg.device_class = cover->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class,
|
||||
ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
||||
@@ -609,9 +650,9 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
|
||||
msg.unit_of_measurement = sensor->get_unit_of_measurement_ref();
|
||||
msg.accuracy_decimals = sensor->get_accuracy_decimals();
|
||||
msg.force_update = sensor->get_force_update();
|
||||
msg.device_class = sensor->get_device_class_ref();
|
||||
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
|
||||
return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class,
|
||||
ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -631,8 +672,8 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
auto *a_switch = static_cast<switch_::Switch *>(entity);
|
||||
ListEntitiesSwitchResponse msg;
|
||||
msg.assumed_state = a_switch->assumed_state();
|
||||
msg.device_class = a_switch->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class,
|
||||
ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||
@@ -661,9 +702,8 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
|
||||
uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
ListEntitiesTextSensorResponse msg;
|
||||
msg.device_class = text_sensor->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(
|
||||
text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -776,11 +816,11 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
|
||||
ListEntitiesNumberResponse msg;
|
||||
msg.unit_of_measurement = number->get_unit_of_measurement_ref();
|
||||
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
|
||||
msg.device_class = number->get_device_class_ref();
|
||||
msg.min_value = number->traits.get_min_value();
|
||||
msg.max_value = number->traits.get_max_value();
|
||||
msg.step = number->traits.get_step();
|
||||
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class,
|
||||
ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
|
||||
@@ -925,8 +965,8 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||
uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *button = static_cast<button::Button *>(entity);
|
||||
ListEntitiesButtonResponse msg;
|
||||
msg.device_class = button->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class,
|
||||
ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(button::Button, button, button)
|
||||
@@ -986,11 +1026,11 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
|
||||
auto *valve = static_cast<valve::Valve *>(entity);
|
||||
ListEntitiesValveResponse msg;
|
||||
auto traits = valve->get_traits();
|
||||
msg.device_class = valve->get_device_class_ref();
|
||||
msg.assumed_state = traits.get_is_assumed_state();
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class,
|
||||
ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
|
||||
@@ -1434,9 +1474,9 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e
|
||||
uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *event = static_cast<event::Event *>(entity);
|
||||
ListEntitiesEventResponse msg;
|
||||
msg.device_class = event->get_device_class_ref();
|
||||
msg.event_types = &event->get_event_types();
|
||||
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class,
|
||||
ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1492,8 +1532,8 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
|
||||
uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *update = static_cast<update::UpdateEntity *>(entity);
|
||||
ListEntitiesUpdateResponse msg;
|
||||
msg.device_class = update->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class,
|
||||
ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
|
||||
|
||||
@@ -334,36 +334,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
// Helper to fill entity info base and encode message
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// API 1.14+ clients compute object_id client-side from the entity name
|
||||
// For older clients, we must send object_id for backward compatibility
|
||||
// See: https://github.com/esphome/backlog/issues/76
|
||||
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
|
||||
// Buffer must remain in scope until encode_message_to_buffer is called
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
if (!conn->client_supports_api_version(1, 14)) {
|
||||
msg.object_id = entity->get_object_id_to(object_id_buf);
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.name = entity->get_name();
|
||||
}
|
||||
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
char icon_buf[MAX_ICON_LENGTH];
|
||||
msg.icon = StringRef(entity->get_icon_to(icon_buf));
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
|
||||
}
|
||||
// Wrapper for entity types that have a device_class field
|
||||
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
StringRef &device_class_field, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
// Helper to check voice assistant validity and connection ownership
|
||||
|
||||
@@ -30,15 +30,11 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
|
||||
|
||||
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->binary_sensor_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available;
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
root[MQTT_PAYLOAD_OFF] = mqtt::global_mqtt_client->get_availability().payload_not_available;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
config.command_topic = false;
|
||||
}
|
||||
bool MQTTBinarySensorComponent::send_initial_state() {
|
||||
|
||||
@@ -30,13 +30,7 @@ void MQTTButtonComponent::dump_config() {
|
||||
}
|
||||
|
||||
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
config.state_topic = false;
|
||||
const auto device_class = this->button_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button")
|
||||
|
||||
@@ -214,6 +214,11 @@ bool MQTTComponent::send_discovery_() {
|
||||
if (icon[0] != '\0') {
|
||||
root[MQTT_ICON] = icon;
|
||||
}
|
||||
char dc_buf[MAX_DEVICE_CLASS_LENGTH];
|
||||
const char *dc = this->get_entity()->get_device_class_to(dc_buf);
|
||||
if (dc[0] != '\0') {
|
||||
root[MQTT_DEVICE_CLASS] = dc;
|
||||
}
|
||||
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
if (entity_category != ENTITY_CATEGORY_NONE) {
|
||||
|
||||
@@ -91,12 +91,6 @@ void MQTTCoverComponent::dump_config() {
|
||||
}
|
||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->cover_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
auto traits = this->cover_->get_traits();
|
||||
if (traits.get_is_assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
@@ -129,6 +123,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
||||
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||
}
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
|
||||
config.command_topic = false;
|
||||
}
|
||||
|
||||
@@ -20,13 +20,6 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
||||
for (const auto &event_type : this->event_->get_event_types())
|
||||
event_types.add(event_type);
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->event_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
config.command_topic = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,10 +57,6 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
root[MQTT_MODE] =
|
||||
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX));
|
||||
}
|
||||
const auto device_class = this->number_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
config.command_topic = true;
|
||||
|
||||
@@ -44,11 +44,6 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->sensor_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
|
||||
if (this->sensor_->has_accuracy_decimals()) {
|
||||
root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals();
|
||||
}
|
||||
|
||||
@@ -14,12 +14,6 @@ using namespace esphome::text_sensor;
|
||||
|
||||
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
||||
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->sensor_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
config.command_topic = false;
|
||||
}
|
||||
void MQTTTextSensor::setup() {
|
||||
|
||||
@@ -64,12 +64,6 @@ void MQTTValveComponent::dump_config() {
|
||||
}
|
||||
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->valve_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
auto traits = this->valve_->get_traits();
|
||||
if (traits.get_is_assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
@@ -78,6 +72,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
||||
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
|
||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve")
|
||||
|
||||
@@ -2137,7 +2137,8 @@ json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef
|
||||
for (const char *event_type : obj->get_event_types()) {
|
||||
event_types.add(event_type);
|
||||
}
|
||||
root[ESPHOME_F("device_class")] = obj->get_device_class_ref();
|
||||
char dc_buf[MAX_DEVICE_CLASS_LENGTH];
|
||||
root[ESPHOME_F("device_class")] = obj->get_device_class_to(dc_buf);
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,12 @@ else:
|
||||
# Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h
|
||||
FRIENDLY_NAME_MAX_LEN = 120
|
||||
|
||||
# Max device class string length (47 chars + null = 48-byte PROGMEM buffer)
|
||||
# Keep in sync with MAX_DEVICE_CLASS_LENGTH in esphome/core/entity_base.h:
|
||||
# DEVICE_CLASS_MAX_LENGTH == MAX_DEVICE_CLASS_LENGTH - 1 (C++ includes the null)
|
||||
DEVICE_CLASS_MAX_LENGTH = 47
|
||||
|
||||
|
||||
# Max icon string length (63 chars + null = 64-byte PROGMEM buffer)
|
||||
# Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h
|
||||
ICON_MAX_LENGTH = 63
|
||||
|
||||
@@ -51,7 +51,27 @@ __attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return "
|
||||
__attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; }
|
||||
__attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; }
|
||||
|
||||
// Entity device class (from index)
|
||||
// Entity device class — buffer-based API for PROGMEM safety on ESP8266
|
||||
const char *EntityBase::get_device_class_to([[maybe_unused]] std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
const uint8_t idx = this->device_class_idx_;
|
||||
#else
|
||||
const uint8_t idx = 0;
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
if (idx == 0)
|
||||
return "";
|
||||
const char *dc = entity_device_class_lookup(idx);
|
||||
ESPHOME_strncpy_P(buffer.data(), dc, buffer.size() - 1);
|
||||
buffer[buffer.size() - 1] = '\0';
|
||||
return buffer.data();
|
||||
#else
|
||||
return entity_device_class_lookup(idx);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef USE_ESP8266
|
||||
// Deprecated device class accessors — not available on ESP8266 (rodata is RAM)
|
||||
StringRef EntityBase::get_device_class_ref() const {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
return StringRef(entity_device_class_lookup(this->device_class_idx_));
|
||||
@@ -59,7 +79,14 @@ StringRef EntityBase::get_device_class_ref() const {
|
||||
return StringRef(entity_device_class_lookup(0));
|
||||
#endif
|
||||
}
|
||||
std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); }
|
||||
std::string EntityBase::get_device_class() const {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
return std::string(entity_device_class_lookup(this->device_class_idx_));
|
||||
#else
|
||||
return std::string(entity_device_class_lookup(0));
|
||||
#endif
|
||||
}
|
||||
#endif // !USE_ESP8266
|
||||
|
||||
// Entity unit of measurement (from index)
|
||||
StringRef EntityBase::get_unit_of_measurement_ref() const {
|
||||
@@ -191,8 +218,10 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj)
|
||||
#endif
|
||||
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_device_class_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str());
|
||||
char dc_buf[MAX_DEVICE_CLASS_LENGTH];
|
||||
const char *dc = obj.get_device_class_to(dc_buf);
|
||||
if (dc[0] != '\0') {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, dc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128;
|
||||
// Maximum state length that Home Assistant will accept without raising ValueError
|
||||
static constexpr size_t MAX_STATE_LEN = 255;
|
||||
|
||||
// Maximum device class string buffer size (47 chars + null terminator)
|
||||
// Longest standard device class: "volatile_organic_compounds_parts" (32 chars)
|
||||
// Device classes are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer.
|
||||
static constexpr size_t MAX_DEVICE_CLASS_LENGTH = 48;
|
||||
|
||||
// Maximum icon string buffer size (63 chars + null terminator)
|
||||
// Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer.
|
||||
static constexpr size_t MAX_ICON_LENGTH = 64;
|
||||
@@ -113,13 +118,31 @@ class EntityBase {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get device class as StringRef (from packed index)
|
||||
// Get this entity's device class into a stack buffer.
|
||||
// On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused).
|
||||
// On ESP8266: copies from PROGMEM to buffer, returns buffer pointer.
|
||||
const char *get_device_class_to(std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed
|
||||
// directly as const char*. Use get_device_class_to() with a stack buffer instead.
|
||||
template<typename T = int> StringRef get_device_class_ref() const {
|
||||
static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). "
|
||||
"Use get_device_class_to() with a stack buffer.");
|
||||
return StringRef("");
|
||||
}
|
||||
template<typename T = int> std::string get_device_class() const {
|
||||
static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). "
|
||||
"Use get_device_class_to() with a stack buffer.");
|
||||
return "";
|
||||
}
|
||||
#else
|
||||
// Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM.
|
||||
ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
|
||||
StringRef get_device_class_ref() const;
|
||||
/// Get the device class as std::string (deprecated, prefer get_device_class_ref())
|
||||
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in "
|
||||
"ESPHome 2026.9.0",
|
||||
"2026.3.0")
|
||||
ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
|
||||
std::string get_device_class() const;
|
||||
#endif
|
||||
// Get unit of measurement as StringRef (from packed index)
|
||||
StringRef get_unit_of_measurement_ref() const;
|
||||
/// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref())
|
||||
|
||||
@@ -17,7 +17,7 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.config import ICON_MAX_LENGTH
|
||||
from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH
|
||||
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case
|
||||
@@ -132,7 +132,7 @@ def _generate_category_code(
|
||||
|
||||
|
||||
_CATEGORY_CONFIGS = (
|
||||
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False),
|
||||
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", True),
|
||||
("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False),
|
||||
("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True),
|
||||
)
|
||||
@@ -179,6 +179,10 @@ def _register_string(
|
||||
|
||||
def register_device_class(value: str) -> int:
|
||||
"""Register a device_class string and return its 1-based index."""
|
||||
if value and len(value) > DEVICE_CLASS_MAX_LENGTH:
|
||||
raise ValueError(
|
||||
f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'"
|
||||
)
|
||||
return _register_string(
|
||||
value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class"
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.core.entity_helpers import (
|
||||
_setup_entity_impl,
|
||||
entity_duplicate_validator,
|
||||
get_base_entity_object_id,
|
||||
register_device_class,
|
||||
register_icon,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -926,6 +927,22 @@ def test_register_icon_max_length() -> None:
|
||||
assert register_icon("") == 0
|
||||
|
||||
|
||||
def test_register_device_class_max_length() -> None:
|
||||
"""Test register_device_class rejects device classes exceeding 47 characters."""
|
||||
# 47 chars should succeed
|
||||
max_dc = "a" * 47
|
||||
idx = register_device_class(max_dc)
|
||||
assert idx > 0
|
||||
|
||||
# 48 chars should fail
|
||||
too_long = "a" * 48
|
||||
with pytest.raises(ValueError, match="Device class string too long"):
|
||||
register_device_class(too_long)
|
||||
|
||||
# Empty string returns 0
|
||||
assert register_device_class("") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_with_entity_category(
|
||||
setup_test_environment: list[str],
|
||||
|
||||
Reference in New Issue
Block a user