[core] Pack entity string properties into PROGMEM-indexed uint8_t fields (#14171)

This commit is contained in:
J. Nick Koston
2026-03-03 07:03:24 -10:00
committed by GitHub
parent d53ff7892a
commit 1f1b20f4fe
51 changed files with 519 additions and 206 deletions
@@ -186,8 +186,8 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
)
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
+2 -2
View File
@@ -773,9 +773,9 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *number = static_cast<number::Number *>(entity);
ListEntitiesNumberResponse msg;
msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref();
msg.unit_of_measurement = number->get_unit_of_measurement_ref();
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
msg.device_class = number->traits.get_device_class_ref();
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();
+7 -5
View File
@@ -60,7 +60,11 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.util import Registry
@@ -604,11 +608,9 @@ async def _build_binary_sensor_automations(var, config):
)
@setup_entity("binary_sensor")
async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config, "binary_sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
setup_device_class(config)
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
CONF_PUBLISH_INITIAL_STATE, False
)
@@ -30,7 +30,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
* The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you.
*/
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
class BinarySensor : public StatefulEntityBase<bool> {
public:
explicit BinarySensor(){};
+7 -5
View File
@@ -18,7 +18,11 @@ from esphome.const import (
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"]
@@ -84,15 +88,13 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema)
@setup_entity("button")
async def setup_button_core_(var, config):
await setup_entity(var, config, "button")
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if device_class := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class))
setup_device_class(config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
+1 -1
View File
@@ -22,7 +22,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
*
* A button is just a momentary switch that does not have a state, only a trigger.
*/
class Button : public EntityBase, public EntityBase_DeviceClass {
class Button : public EntityBase {
public:
/** Press this button. This is called by the front-end.
*
+1 -2
View File
@@ -268,9 +268,8 @@ def climate_schema(
return _CLIMATE_SCHEMA.extend(schema)
@setup_entity("climate")
async def setup_climate_core_(var, config):
await setup_entity(var, config, "climate")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
+7 -5
View File
@@ -37,7 +37,11 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, TemplateArgsType
@@ -190,11 +194,9 @@ def cover_schema(
return _COVER_SCHEMA.extend(schema)
@setup_entity("cover")
async def setup_cover_core_(var, config):
await setup_entity(var, config, "cover")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
setup_device_class(config)
if CONF_ON_OPEN in config:
_LOGGER.warning(
+1 -1
View File
@@ -107,7 +107,7 @@ const LogString *cover_operation_to_str(CoverOperation op);
* to control all values of the cover. Also implement get_traits() to return what operations
* the cover supports.
*/
class Cover : public EntityBase, public EntityBase_DeviceClass {
class Cover : public EntityBase {
public:
explicit Cover();
+1 -2
View File
@@ -134,9 +134,8 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
return _DATETIME_SCHEMA.extend(schema)
@setup_entity("datetime")
async def setup_datetime_core_(var, config):
await setup_entity(var, config, "datetime")
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
+1
View File
@@ -48,6 +48,7 @@ void arch_init() {
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
+3
View File
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
+7 -5
View File
@@ -18,7 +18,11 @@ from esphome.const import (
DEVICE_CLASS_MOTION,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@nohat"]
@@ -85,17 +89,15 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema)
@setup_entity("event")
async def setup_event_core_(var, config, *, event_types: list[str]):
await setup_entity(var, config, "event")
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
cg.add(var.set_event_types(event_types))
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
setup_device_class(config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
+1 -1
View File
@@ -20,7 +20,7 @@ namespace event {
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
}
class Event : public EntityBase, public EntityBase_DeviceClass {
class Event : public EntityBase {
public:
void trigger(const std::string &event_type);
+1 -2
View File
@@ -222,9 +222,8 @@ def validate_preset_modes(value):
return value
@setup_entity("fan")
async def setup_fan_core_(var, config):
await setup_entity(var, config, "fan")
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
+1
View File
@@ -59,6 +59,7 @@ void HOT arch_feed_wdt() {
}
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() {
struct timespec spec;
+1 -1
View File
@@ -45,9 +45,9 @@ def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema:
)
@setup_entity("infrared")
async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up core infrared configuration."""
await setup_entity(var, config, "infrared")
async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
+1
View File
@@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
} // namespace esphome
+3 -4
View File
@@ -243,9 +243,8 @@ def validate_color_temperature_channels(value):
return value
async def setup_light_core_(light_var, output_var, config):
await setup_entity(light_var, config, "light")
@setup_entity("light")
async def setup_light_core_(light_var, config, output_var):
cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))
if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None:
@@ -312,7 +311,7 @@ async def register_light(output_var, config):
cg.add(cg.App.register_light(light_var))
CORE.register_platform_component("light", light_var)
await cg.register_component(light_var, config)
await setup_light_core_(light_var, output_var, config)
await setup_light_core_(light_var, config, output_var)
async def new_light(config, *args):
+1 -2
View File
@@ -91,9 +91,8 @@ def lock_schema(
return _LOCK_SCHEMA.extend(schema)
@setup_entity("lock")
async def _setup_lock_core(var, config):
await setup_entity(var, config, "lock")
for conf in config.get(CONF_ON_LOCK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
+1 -1
View File
@@ -96,8 +96,8 @@ VolumeSetAction = media_player_ns.class_(
)
@setup_entity("media_player")
async def setup_media_player_core_(var, config):
await setup_entity(var, config, "media_player")
for conf_key, _ in _STATE_TRIGGERS:
for conf in config.get(conf_key, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+2 -2
View File
@@ -48,7 +48,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
root[MQTT_MAX] = traits.get_max_value();
root[MQTT_STEP] = traits.get_step();
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref();
const auto unit_of_measurement = this->number_->get_unit_of_measurement_ref();
if (!unit_of_measurement.empty()) {
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
}
@@ -57,7 +57,7 @@ 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_->traits.get_device_class_ref();
const auto device_class = this->number_->get_device_class_ref();
if (!device_class.empty()) {
root[MQTT_DEVICE_CLASS] = device_class;
}
+9 -7
View File
@@ -79,7 +79,12 @@ from esphome.const import (
DEVICE_CLASS_WIND_SPEED,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
setup_unit_of_measurement,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"]
@@ -257,11 +262,10 @@ async def _build_number_automations(var, config):
await automation.build_automation(trigger, [(float, "x")], conf)
@setup_entity("number")
async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float
):
await setup_entity(var, config, "number")
cg.add(var.traits.set_min_value(min_value))
cg.add(var.traits.set_max_value(max_value))
cg.add(var.traits.set_step(step))
@@ -273,10 +277,8 @@ async def setup_number_core_(
CORE.add_job(_build_number_automations, var, config)
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
cg.add(var.traits.set_unit_of_measurement(unit_of_measurement))
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.traits.set_device_class(device_class))
setup_device_class(config)
setup_unit_of_measurement(config)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
+2 -2
View File
@@ -15,8 +15,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_ICON(tag, prefix, *obj);
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits);
LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits);
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, *obj);
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
}
void Number::publish_state(float state) {
+3 -3
View File
@@ -1,7 +1,7 @@
#pragma once
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include <cmath>
#include <cstdint>
namespace esphome::number {
@@ -11,7 +11,7 @@ enum NumberMode : uint8_t {
NUMBER_MODE_SLIDER = 2,
};
class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement {
class NumberTraits {
public:
// Set/get the number value boundaries.
void set_min_value(float min_value) { min_value_ = min_value; }
+3
View File
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { watchdog_update(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
+1 -2
View File
@@ -92,9 +92,8 @@ def select_schema(
return _SELECT_SCHEMA.extend(schema)
@setup_entity("select")
async def setup_select_core_(var, config, *, options: list[str]):
await setup_entity(var, config, "select")
cg.add(var.traits.set_options(options))
for conf in config.get(CONF_ON_VALUE, []):
+9 -7
View File
@@ -106,7 +106,12 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
setup_unit_of_measurement,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.util import Registry
@@ -908,15 +913,12 @@ async def _build_sensor_automations(var, config):
await automation.build_automation(trigger, [(float, "x")], conf)
@setup_entity("sensor")
async def setup_sensor_core_(var, config):
await setup_entity(var, config, "sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
setup_device_class(config)
setup_unit_of_measurement(config)
if (state_class := config.get(CONF_STATE_CLASS)) is not None:
cg.add(var.set_state_class(state_class))
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
cg.add(var.set_unit_of_measurement(unit_of_measurement))
if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None:
cg.add(var.set_accuracy_decimals(accuracy_decimals))
# Only set force_update if True (default is False)
+1 -1
View File
@@ -44,7 +44,7 @@ const LogString *state_class_to_string(StateClass state_class);
*
* A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy.
*/
class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement {
class Sensor : public EntityBase {
public:
explicit Sensor();
+2 -2
View File
@@ -567,7 +567,7 @@ void Sprinkler::set_valve_run_duration(const optional<size_t> valve_number, cons
return;
}
auto call = this->valve_[valve_number.value()].run_duration_number->make_call();
if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
if (this->valve_[valve_number.value()].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
call.set_value(run_duration.value() / 60.0);
} else {
call.set_value(run_duration.value());
@@ -649,7 +649,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
return 0;
}
if (this->valve_[valve_number].run_duration_number != nullptr) {
if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
if (this->valve_[valve_number].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state * 60));
} else {
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state));
+7 -5
View File
@@ -22,7 +22,11 @@ from esphome.const import (
DEVICE_CLASS_SWITCH,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"]
@@ -154,9 +158,8 @@ async def _build_switch_automations(var, config):
await automation.build_automation(trigger, [], conf)
@setup_entity("switch")
async def setup_switch_core_(var, config):
await setup_entity(var, config, "switch")
if (inverted := config.get(CONF_INVERTED)) is not None:
cg.add(var.set_inverted(inverted))
@@ -169,8 +172,7 @@ async def setup_switch_core_(var, config):
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
setup_device_class(config)
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
await zigbee.setup_switch(var, config)
+1 -1
View File
@@ -35,7 +35,7 @@ enum SwitchRestoreMode : uint8_t {
* A switch is basically just a combination of a binary sensor (for reporting switch values)
* and a write_state method that writes a state to the hardware.
*/
class Switch : public EntityBase, public EntityBase_DeviceClass {
class Switch : public EntityBase {
public:
explicit Switch();
+1 -2
View File
@@ -84,6 +84,7 @@ def text_schema(
return _TEXT_SCHEMA.extend(schema)
@setup_entity("text")
async def setup_text_core_(
var,
config,
@@ -92,8 +93,6 @@ async def setup_text_core_(
max_length: int | None,
pattern: str | None,
):
await setup_entity(var, config, "text")
cg.add(var.traits.set_min_length(min_length))
cg.add(var.traits.set_max_length(max_length))
if pattern is not None:
+7 -5
View File
@@ -21,7 +21,11 @@ from esphome.const import (
DEVICE_CLASS_TIMESTAMP,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.util import Registry
@@ -208,11 +212,9 @@ async def _build_text_sensor_automations(var, config):
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
@setup_entity("text_sensor")
async def setup_text_sensor_core_(var, config):
await setup_entity(var, config, "text_sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
setup_device_class(config)
if config.get(CONF_FILTERS): # must exist and not be empty
cg.add_define("USE_TEXT_SENSOR_FILTER")
+1 -1
View File
@@ -25,7 +25,7 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
public: \
void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; }
class TextSensor : public EntityBase, public EntityBase_DeviceClass {
class TextSensor : public EntityBase {
public:
std::string state;
+7 -5
View File
@@ -15,7 +15,11 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@jesserockz"]
@@ -87,11 +91,9 @@ def update_schema(
return _UPDATE_SCHEMA.extend(schema)
@setup_entity("update")
async def setup_update_core_(var, config):
await setup_entity(var, config, "update")
if device_class_config := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class_config))
setup_device_class(config)
if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE):
await automation.build_automation(
+1 -1
View File
@@ -29,7 +29,7 @@ enum UpdateState : uint8_t {
const LogString *update_state_to_string(UpdateState state);
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
class UpdateEntity : public EntityBase {
public:
void publish_state();
+7 -5
View File
@@ -22,7 +22,11 @@ from esphome.const import (
DEVICE_CLASS_WATER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
IS_PLATFORM_COMPONENT = True
@@ -129,11 +133,9 @@ def valve_schema(
return _VALVE_SCHEMA.extend(schema)
@setup_entity("valve")
async def _setup_valve_core(var, config):
await setup_entity(var, config, "valve")
if device_class_config := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class_config))
setup_device_class(config)
for conf in config.get(CONF_ON_OPEN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+1 -1
View File
@@ -101,7 +101,7 @@ const LogString *valve_operation_to_str(ValveOperation op);
* to control all values of the valve. Also implement get_traits() to return what operations
* the valve supports.
*/
class Valve : public EntityBase, public EntityBase_DeviceClass {
class Valve : public EntityBase {
public:
explicit Valve();
+1 -2
View File
@@ -69,10 +69,9 @@ def water_heater_schema(
return _WATER_HEATER_SCHEMA.extend(schema)
@setup_entity("water_heater")
async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up the core water heater properties in C++."""
await setup_entity(var, config, "water_heater")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
+1 -1
View File
@@ -1139,7 +1139,7 @@ json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float v
json::JsonBuilder builder;
JsonObject root = builder.root();
const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
const auto uom_ref = obj->get_unit_of_measurement_ref();
const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
// Need two buffers: one for value, one for state with UOM
+1
View File
@@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
Mutex::Mutex() {
+2
View File
@@ -44,7 +44,9 @@
#define USE_DEEP_SLEEP
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_DEVICE_CLASS
#define USE_ENTITY_ICON
#define USE_ENTITY_UNIT_OF_MEASUREMENT
#define USE_ESP32_CAMERA_JPEG_CONVERSION
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
+32 -32
View File
@@ -45,24 +45,42 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
}
}
// Entity Icon
std::string EntityBase::get_icon() const {
#ifdef USE_ENTITY_ICON
if (this->icon_c_str_ == nullptr) {
return "";
}
return this->icon_c_str_;
// Weak default lookup functions — overridden by generated code in main.cpp
__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)
StringRef EntityBase::get_device_class_ref() const {
#ifdef USE_ENTITY_DEVICE_CLASS
return StringRef(entity_device_class_lookup(this->device_class_idx_));
#else
return "";
return StringRef(entity_device_class_lookup(0));
#endif
}
void EntityBase::set_icon(const char *icon) {
#ifdef USE_ENTITY_ICON
this->icon_c_str_ = icon;
std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); }
// Entity unit of measurement (from index)
StringRef EntityBase::get_unit_of_measurement_ref() const {
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
return StringRef(entity_uom_lookup(this->uom_idx_));
#else
// No-op when USE_ENTITY_ICON is not defined
return StringRef(entity_uom_lookup(0));
#endif
}
std::string EntityBase::get_unit_of_measurement() const {
return std::string(this->get_unit_of_measurement_ref().c_str());
}
// Entity icon (from index)
StringRef EntityBase::get_icon_ref() const {
#ifdef USE_ENTITY_ICON
return StringRef(entity_icon_lookup(this->icon_idx_));
#else
return StringRef(entity_icon_lookup(0));
#endif
}
std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); }
// Entity Object ID - computed on-demand from name
std::string EntityBase::get_object_id() const {
@@ -134,24 +152,6 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve
return global_preferences->make_preference(size, key);
}
std::string EntityBase_DeviceClass::get_device_class() {
if (this->device_class_ == nullptr) {
return "";
}
return this->device_class_;
}
void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; }
std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() {
if (this->unit_of_measurement_ == nullptr)
return "";
return this->unit_of_measurement_;
}
void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_measurement) {
this->unit_of_measurement_ = unit_of_measurement;
}
#ifdef USE_ENTITY_ICON
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
if (!obj.get_icon_ref().empty()) {
@@ -160,13 +160,13 @@ 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_DeviceClass &obj) {
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());
}
}
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj) {
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj) {
if (!obj.get_unit_of_measurement_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj.get_unit_of_measurement_ref().c_str());
}
+48 -52
View File
@@ -14,6 +14,12 @@
namespace esphome {
// Extern lookup functions for entity string tables.
// Generated code provides strong definitions; weak defaults return "".
extern const char *entity_device_class_lookup(uint8_t index);
extern const char *entity_uom_lookup(uint8_t index);
extern const char *entity_icon_lookup(uint8_t index);
// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py
static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31;
@@ -89,20 +95,41 @@ class EntityBase {
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
}
// Set entity string table indices — one call per entity from codegen.
// Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits)
void set_entity_strings([[maybe_unused]] uint32_t packed) {
#ifdef USE_ENTITY_DEVICE_CLASS
this->device_class_idx_ = packed & 0xFF;
#endif
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
this->uom_idx_ = (packed >> 8) & 0xFF;
#endif
#ifdef USE_ENTITY_ICON
this->icon_idx_ = (packed >> 16) & 0xFF;
#endif
}
// Get device class as StringRef (from packed index)
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")
std::string get_device_class() const;
// 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())
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
"removed in ESPHome 2026.9.0",
"2026.3.0")
std::string get_unit_of_measurement() const;
// Get/set this entity's icon
ESPDEPRECATED(
"Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0",
"2025.11.0")
std::string get_icon() const;
void set_icon(const char *icon);
StringRef get_icon_ref() const {
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
#ifdef USE_ENTITY_ICON
return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_);
#else
return EMPTY_STRING;
#endif
}
StringRef get_icon_ref() const;
#ifdef USE_DEVICES
// Get/set this entity's device id
@@ -173,9 +200,6 @@ class EntityBase {
void calc_object_id_();
StringRef name_;
#ifdef USE_ENTITY_ICON
const char *icon_c_str_{nullptr};
#endif
uint32_t object_id_hash_{};
#ifdef USE_DEVICES
Device *device_{};
@@ -190,44 +214,16 @@ class EntityBase {
uint8_t entity_category : 2; // Supports up to 4 categories
uint8_t reserved : 2; // Reserved for future use
} flags_{};
};
class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming)
public:
/// Get the device class, using the manual override if set.
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in "
"ESPHome 2026.5.0",
"2025.11.0")
std::string get_device_class();
/// Manually set the device class.
void set_device_class(const char *device_class);
/// Get the device class as StringRef
StringRef get_device_class_ref() const {
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_);
}
protected:
const char *device_class_{nullptr}; ///< Device class override
};
class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
public:
/// Get the unit of measurement, using the manual override if set.
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
"removed in ESPHome 2026.5.0",
"2025.11.0")
std::string get_unit_of_measurement();
/// Manually set the unit of measurement.
void set_unit_of_measurement(const char *unit_of_measurement);
/// Get the unit of measurement as StringRef
StringRef get_unit_of_measurement_ref() const {
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_);
}
protected:
const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override
// String table indices — packed into the 3 padding bytes after flags_
#ifdef USE_ENTITY_DEVICE_CLASS
uint8_t device_class_idx_{};
#endif
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
uint8_t uom_idx_{};
#endif
#ifdef USE_ENTITY_ICON
uint8_t icon_idx_{};
#endif
};
/// Log entity icon if set (for use in dump_config)
@@ -240,10 +236,10 @@ inline void log_entity_icon(const char *, const char *, const EntityBase &) {}
#endif
/// Log entity device class if set (for use in dump_config)
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj);
/// Log entity unit of measurement if set (for use in dump_config)
#define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj)
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj);
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj);
/**
* An entity that has a state.
+221 -6
View File
@@ -1,9 +1,12 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import functools
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_DEVICE_ID,
CONF_DISABLED_BY_DEFAULT,
CONF_ENTITY_CATEGORY,
@@ -11,15 +14,184 @@ from esphome.const import (
CONF_ID,
CONF_INTERNAL,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, add, get_variable
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
import esphome.final_validate as fv
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case
from esphome.types import ConfigType, EntityMetadata
_LOGGER = logging.getLogger(__name__)
DOMAIN = "entity_string_pool"
# Private config keys for storing registered string indices
_KEY_DC_IDX = "_entity_dc_idx"
_KEY_UOM_IDX = "_entity_uom_idx"
_KEY_ICON_IDX = "_entity_icon_idx"
# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h:
# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits)
_DC_SHIFT = 0
_UOM_SHIFT = 8
_ICON_SHIFT = 16
# Maximum unique strings per category (8-bit index, 0 = not set)
_MAX_DEVICE_CLASSES = 0xFF # 255
_MAX_UNITS = 0xFF # 255
_MAX_ICONS = 0xFF # 255
@dataclass
class EntityStringPool:
"""Pool of entity string properties for PROGMEM pointer tables.
Strings are registered during to_code() and assigned 1-based indices.
Index 0 means "not set" (empty string). At render time, the pool
generates C++ PROGMEM pointer table + lookup function per category.
"""
device_classes: dict[str, int] = field(default_factory=dict)
units: dict[str, int] = field(default_factory=dict)
icons: dict[str, int] = field(default_factory=dict)
tables_registered: bool = False
def _get_pool() -> EntityStringPool:
"""Get or create the entity string pool from CORE.data."""
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = EntityStringPool()
return CORE.data[DOMAIN]
def _ensure_tables_registered() -> None:
"""Schedule the table generation job (once)."""
pool = _get_pool()
if pool.tables_registered:
return
pool.tables_registered = True
CORE.add_job(_generate_tables_job)
def _generate_category_code(
table_var: str,
lookup_fn: str,
strings: dict[str, int],
) -> str:
"""Generate C++ code for one string category (PROGMEM pointer table + lookup).
Uses a PROGMEM array of string pointers. On ESP8266, pointers are stored
in flash (via PROGMEM) and read with progmem_read_ptr(). String literals
themselves remain in RAM but benefit from linker string deduplication.
Index 0 means "not set" and returns empty string.
"""
if not strings:
return ""
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings)
count = len(sorted_strings)
return (
f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n"
f"const char *{lookup_fn}(uint8_t index) {{\n"
f' if (index == 0 || index > {count}) return "";\n'
f" return progmem_read_ptr(&{table_var}[index - 1]);\n"
f"}}\n"
)
_CATEGORY_CONFIGS = (
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"),
("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"),
("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"),
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _generate_tables_job() -> None:
"""Generate all entity string table C++ code as a FINAL-priority job.
Runs after all component to_code() calls have registered their strings.
"""
pool = _get_pool()
parts = ["namespace esphome {"]
for table_var, lookup_fn, attr in _CATEGORY_CONFIGS:
code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr))
if code:
parts.append(code)
parts.append("} // namespace esphome")
cg.add_global(RawStatement("\n".join(parts)))
def _register_string(
value: str, category: dict[str, int], max_count: int, category_name: str
) -> int:
"""Register a string in a category dict and return its 1-based index.
Returns 0 if value is empty/None (meaning "not set").
"""
if not value:
return 0
if value in category:
return category[value]
idx = len(category) + 1
if idx > max_count:
raise ValueError(
f"Too many unique {category_name} values (max {max_count}), got {idx}: '{value}'"
)
category[value] = idx
_ensure_tables_registered()
return idx
def register_device_class(value: str) -> int:
"""Register a device_class string and return its 1-based index."""
return _register_string(
value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class"
)
def register_unit_of_measurement(value: str) -> int:
"""Register a unit_of_measurement string and return its 1-based index."""
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
def register_icon(value: str) -> int:
"""Register an icon string and return its 1-based index."""
return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon")
def setup_device_class(config: ConfigType) -> None:
"""Register config's device_class and store its index for finalize_entity_strings."""
idx = register_device_class(config.get(CONF_DEVICE_CLASS, ""))
if idx:
cg.add_define("USE_ENTITY_DEVICE_CLASS")
config[_KEY_DC_IDX] = idx
def setup_unit_of_measurement(config: ConfigType) -> None:
"""Register config's unit_of_measurement and store its index for finalize_entity_strings."""
idx = register_unit_of_measurement(config.get(CONF_UNIT_OF_MEASUREMENT, ""))
if idx:
cg.add_define("USE_ENTITY_UNIT_OF_MEASUREMENT")
config[_KEY_UOM_IDX] = idx
def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
"""Emit a single set_entity_strings() call with all packed indices.
Call this at the end of each component's setup function, after
setup_entity() and any register_device_class/register_unit_of_measurement calls.
"""
dc_idx = config.get(_KEY_DC_IDX, 0)
uom_idx = config.get(_KEY_UOM_IDX, 0)
icon_idx = config.get(_KEY_ICON_IDX, 0)
packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT)
if packed != 0:
add(var.set_entity_strings(packed))
def get_base_entity_object_id(
name: str, friendly_name: str | None, device_name: str | None = None
@@ -64,8 +236,48 @@ def get_base_entity_object_id(
return sanitize(snake_case(base_str))
async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
"""Set up generic properties of an Entity.
def setup_entity(var_or_platform, config=None, platform=None):
"""Set up entity properties — works as both decorator and direct call.
Decorator mode::
@setup_entity("sensor")
async def setup_sensor_core_(var, config):
setup_device_class(config)
setup_unit_of_measurement(config)
...
Direct call mode (for entities with no extra string properties)::
await setup_entity(var, config, "camera")
"""
if isinstance(var_or_platform, str) and config is None:
# Decorator mode: @setup_entity("sensor")
platform = var_or_platform
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(
var: MockObj, config: ConfigType, *args, **kwargs
) -> None:
await _setup_entity_impl(var, config, platform)
await func(var, config, *args, **kwargs)
finalize_entity_strings(var, config)
return wrapper
return decorator
# Direct call mode: await setup_entity(var, config, "camera")
async def _do() -> None:
await _setup_entity_impl(var_or_platform, config, platform)
finalize_entity_strings(var_or_platform, config)
return _do()
async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> None:
"""Set up generic properties of an Entity (internal implementation).
This function sets up the common entity properties like name, icon,
entity category, etc.
@@ -92,12 +304,15 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
add(var.set_disabled_by_default(True))
if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL]))
icon_idx = 0
if CONF_ICON in config:
# Add USE_ENTITY_ICON define when icons are used
cg.add_define("USE_ENTITY_ICON")
add(var.set_icon(config[CONF_ICON]))
icon_idx = register_icon(config[CONF_ICON])
if CONF_ENTITY_CATEGORY in config:
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
# Store icon index for finalize_entity_strings
config[_KEY_ICON_IDX] = icon_idx
def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
+1
View File
@@ -42,6 +42,7 @@ void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
uint32_t arch_get_cpu_freq_hz();
uint8_t progmem_read_byte(const uint8_t *addr);
const char *progmem_read_ptr(const char *const *addr);
uint16_t progmem_read_uint16(const uint16_t *addr);
} // namespace esphome
+1
View File
@@ -79,6 +79,7 @@ def clang_options(idedata):
"-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))",
"-Dpgm_read_word(s)=(*(const uint16_t *)(s))",
"-Dpgm_read_dword(s)=(*(const uint32_t *)(s))",
"-Dpgm_read_ptr(s)=(*(const void *const *)(s))",
"-DPROGMEM=",
"-DPGM_P=const char *",
"-DPSTR(s)=(s)",
+1 -1
View File
@@ -11,4 +11,4 @@ def test_sensor_device_class_set(generate_main):
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
# Then
assert 's_1->set_device_class("voltage");' in main_cpp
assert "s_1->set_entity_strings(" in main_cpp
@@ -54,5 +54,5 @@ def test_text_sensor_device_class_set(generate_main):
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert 'ts_2->set_device_class("timestamp");' in main_cpp
assert 'ts_3->set_device_class("date");' in main_cpp
assert "ts_2->set_entity_strings(" in main_cpp
assert "ts_3->set_entity_strings(" in main_cpp

Some files were not shown because too many files have changed in this diff Show More