move device class strings to PROGMEM on ESP8266

Same treatment as icons in #14437. Device class strings were in
.rodata (RAM on ESP8266). Now stored as individual PROGMEM char arrays.

- Add get_device_class_to(std::span<char, MAX_DEVICE_CLASS_LENGTH>)
  buffer API matching get_icon_to() pattern
- Add fill_and_encode_entity_info_with_device_class() wrapper to
  deduplicate buffer handling across 10 API entity types
- Centralize MQTT device_class in send_discovery_() lambda where
  buffer lifetime outlives ArduinoJson serialization
- Deprecate get_device_class_ref()/get_device_class() on non-ESP8266
- static_assert error on ESP8266 directing to get_device_class_to()
- Update all callers: api, mqtt, web_server, log helper
This commit is contained in:
J. Nick Koston
2026-03-03 10:50:39 -10:00
parent afb8ba6813
commit ca6a05bfdd
15 changed files with 98 additions and 81 deletions
+20 -22
View File
@@ -413,10 +413,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
@@ -442,8 +441,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)
@@ -608,9 +607,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
@@ -630,8 +629,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)
@@ -660,9 +659,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
@@ -775,11 +773,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)
@@ -924,8 +922,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)
@@ -985,11 +983,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)
@@ -1433,9 +1431,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
@@ -1491,8 +1489,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)
+9
View File
@@ -359,6 +359,15 @@ class APIConnection final : public APIServerConnectionBase {
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) {
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_VOICE_ASSISTANT
// Helper to check voice assistant validity and connection ownership
inline bool check_voice_assistant_api_connection_() const;
@@ -29,12 +29,6 @@ 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())
-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) {
-7
View File
@@ -90,13 +90,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;
-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() {
-7
View File
@@ -63,13 +63,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;
+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);
}
+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 ESP32: 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())
+1 -1
View File
@@ -131,7 +131,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),
)