[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:
J. Nick Koston
2026-03-06 08:34:27 -10:00
committed by GitHub
parent b2378e830e
commit 8a915dcbbe
17 changed files with 167 additions and 108 deletions
+62 -22
View File
@@ -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)
+5 -29
View File
@@ -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() {
-6
View File
@@ -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) {
+1 -6
View File
@@ -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;
}
-7
View File
@@ -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;
}
-4
View File
@@ -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;
-5
View File
@@ -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() {
+1 -6
View File
@@ -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")
+2 -1
View File
@@ -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);
}
+6
View File
@@ -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
+33 -4
View File
@@ -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);
}
}
+28 -5
View File
@@ -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())
+6 -2
View File
@@ -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],