[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): 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, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) 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) { uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *number = static_cast<number::Number *>(entity); auto *number = static_cast<number::Number *>(entity);
ListEntitiesNumberResponse msg; 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.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.min_value = number->traits.get_min_value();
msg.max_value = number->traits.get_max_value(); msg.max_value = number->traits.get_max_value();
msg.step = number->traits.get_step(); msg.step = number->traits.get_step();
+7 -5
View File
@@ -60,7 +60,11 @@ from esphome.const import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
from esphome.util import Registry 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): async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config, "binary_sensor") setup_device_class(config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
CONF_PUBLISH_INITIAL_STATE, False 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 * The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you. * handles inverted inputs for you.
*/ */
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass { class BinarySensor : public StatefulEntityBase<bool> {
public: public:
explicit BinarySensor(){}; explicit BinarySensor(){};
+7 -5
View File
@@ -18,7 +18,11 @@ from esphome.const import (
DEVICE_CLASS_UPDATE, DEVICE_CLASS_UPDATE,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -84,15 +88,13 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema) return _BUTTON_SCHEMA.extend(schema)
@setup_entity("button")
async def setup_button_core_(var, config): async def setup_button_core_(var, config):
await setup_entity(var, config, "button")
for conf in config.get(CONF_ON_PRESS, []): for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
if device_class := config.get(CONF_DEVICE_CLASS): setup_device_class(config)
cg.add(var.set_device_class(device_class))
if mqtt_id := config.get(CONF_MQTT_ID): if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var) 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. * 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: public:
/** Press this button. This is called by the front-end. /** 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) return _CLIMATE_SCHEMA.extend(schema)
@setup_entity("climate")
async def setup_climate_core_(var, config): async def setup_climate_core_(var, config):
await setup_entity(var, config, "climate")
visual = config[CONF_VISUAL] visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
+7 -5
View File
@@ -37,7 +37,11 @@ from esphome.const import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, TemplateArgsType from esphome.types import ConfigType, TemplateArgsType
@@ -190,11 +194,9 @@ def cover_schema(
return _COVER_SCHEMA.extend(schema) return _COVER_SCHEMA.extend(schema)
@setup_entity("cover")
async def setup_cover_core_(var, config): async def setup_cover_core_(var, config):
await setup_entity(var, config, "cover") setup_device_class(config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
if CONF_ON_OPEN in config: if CONF_ON_OPEN in config:
_LOGGER.warning( _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 * to control all values of the cover. Also implement get_traits() to return what operations
* the cover supports. * the cover supports.
*/ */
class Cover : public EntityBase, public EntityBase_DeviceClass { class Cover : public EntityBase {
public: public:
explicit Cover(); explicit Cover();
+1 -2
View File
@@ -134,9 +134,8 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
return _DATETIME_SCHEMA.extend(schema) return _DATETIME_SCHEMA.extend(schema)
@setup_entity("datetime")
async def setup_datetime_core_(var, config): async def setup_datetime_core_(var, config):
await setup_entity(var, config, "datetime")
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) 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(); } void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } 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; } 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_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { 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) { uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT 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) { uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT return pgm_read_word(addr); // NOLINT
} }
+7 -5
View File
@@ -18,7 +18,11 @@ from esphome.const import (
DEVICE_CLASS_MOTION, DEVICE_CLASS_MOTION,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
CODEOWNERS = ["@nohat"] CODEOWNERS = ["@nohat"]
@@ -85,17 +89,15 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema) return _EVENT_SCHEMA.extend(schema)
@setup_entity("event")
async def setup_event_core_(var, config, *, event_types: list[str]): 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, []): for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
cg.add(var.set_event_types(event_types)) cg.add(var.set_event_types(event_types))
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: setup_device_class(config)
cg.add(var.set_device_class(device_class))
if mqtt_id := config.get(CONF_MQTT_ID): if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
+1 -1
View File
@@ -20,7 +20,7 @@ namespace event {
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
} }
class Event : public EntityBase, public EntityBase_DeviceClass { class Event : public EntityBase {
public: public:
void trigger(const std::string &event_type); void trigger(const std::string &event_type);
+1 -2
View File
@@ -222,9 +222,8 @@ def validate_preset_modes(value):
return value return value
@setup_entity("fan")
async def setup_fan_core_(var, config): async def setup_fan_core_(var, config):
await setup_entity(var, config, "fan")
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: 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; } 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; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { uint32_t arch_get_cpu_cycle_count() {
struct timespec spec; 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: async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up core infrared configuration.""" """Set up core infrared configuration."""
await setup_entity(var, config, "infrared")
async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None: 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_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } 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; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
} // namespace esphome } // namespace esphome
+3 -4
View File
@@ -243,9 +243,8 @@ def validate_color_temperature_channels(value):
return value return value
async def setup_light_core_(light_var, output_var, config): @setup_entity("light")
await setup_entity(light_var, config, "light") async def setup_light_core_(light_var, config, output_var):
cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))
if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: 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)) cg.add(cg.App.register_light(light_var))
CORE.register_platform_component("light", light_var) CORE.register_platform_component("light", light_var)
await cg.register_component(light_var, config) 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): async def new_light(config, *args):
+1 -2
View File
@@ -91,9 +91,8 @@ def lock_schema(
return _LOCK_SCHEMA.extend(schema) return _LOCK_SCHEMA.extend(schema)
@setup_entity("lock")
async def _setup_lock_core(var, config): async def _setup_lock_core(var, config):
await setup_entity(var, config, "lock")
for conf in config.get(CONF_ON_LOCK, []): for conf in config.get(CONF_ON_LOCK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) 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): async def setup_media_player_core_(var, config):
await setup_entity(var, config, "media_player")
for conf_key, _ in _STATE_TRIGGERS: for conf_key, _ in _STATE_TRIGGERS:
for conf in config.get(conf_key, []): for conf in config.get(conf_key, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 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_MAX] = traits.get_max_value();
root[MQTT_STEP] = traits.get_step(); root[MQTT_STEP] = traits.get_step();
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // 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()) { if (!unit_of_measurement.empty()) {
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
} }
@@ -57,7 +57,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
root[MQTT_MODE] = root[MQTT_MODE] =
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX)); 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()) { if (!device_class.empty()) {
root[MQTT_DEVICE_CLASS] = device_class; root[MQTT_DEVICE_CLASS] = device_class;
} }
+9 -7
View File
@@ -79,7 +79,12 @@ from esphome.const import (
DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WIND_SPEED,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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 from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -257,11 +262,10 @@ async def _build_number_automations(var, config):
await automation.build_automation(trigger, [(float, "x")], conf) await automation.build_automation(trigger, [(float, "x")], conf)
@setup_entity("number")
async def setup_number_core_( async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float 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_min_value(min_value))
cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_max_value(max_value))
cg.add(var.traits.set_step(step)) cg.add(var.traits.set_step(step))
@@ -273,10 +277,8 @@ async def setup_number_core_(
CORE.add_job(_build_number_automations, var, config) CORE.add_job(_build_number_automations, var, config)
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: setup_device_class(config)
cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) setup_unit_of_measurement(config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.traits.set_device_class(device_class))
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var) 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()); ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_ICON(tag, prefix, *obj); LOG_ENTITY_ICON(tag, prefix, *obj);
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits); LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, *obj);
LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits); LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
} }
void Number::publish_state(float state) { void Number::publish_state(float state) {
+3 -3
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
#include "esphome/core/entity_base.h" #include <cmath>
#include "esphome/core/helpers.h" #include <cstdint>
namespace esphome::number { namespace esphome::number {
@@ -11,7 +11,7 @@ enum NumberMode : uint8_t {
NUMBER_MODE_SLIDER = 2, NUMBER_MODE_SLIDER = 2,
}; };
class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { class NumberTraits {
public: public:
// Set/get the number value boundaries. // Set/get the number value boundaries.
void set_min_value(float min_value) { min_value_ = min_value; } 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) { uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT 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; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } 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) return _SELECT_SCHEMA.extend(schema)
@setup_entity("select")
async def setup_select_core_(var, config, *, options: list[str]): async def setup_select_core_(var, config, *, options: list[str]):
await setup_entity(var, config, "select")
cg.add(var.traits.set_options(options)) cg.add(var.traits.set_options(options))
for conf in config.get(CONF_ON_VALUE, []): for conf in config.get(CONF_ON_VALUE, []):
+9 -7
View File
@@ -106,7 +106,12 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_CONFIG,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObj, MockObjClass
from esphome.util import Registry from esphome.util import Registry
@@ -908,15 +913,12 @@ async def _build_sensor_automations(var, config):
await automation.build_automation(trigger, [(float, "x")], conf) await automation.build_automation(trigger, [(float, "x")], conf)
@setup_entity("sensor")
async def setup_sensor_core_(var, config): async def setup_sensor_core_(var, config):
await setup_entity(var, config, "sensor") setup_device_class(config)
setup_unit_of_measurement(config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
if (state_class := config.get(CONF_STATE_CLASS)) is not None: if (state_class := config.get(CONF_STATE_CLASS)) is not None:
cg.add(var.set_state_class(state_class)) 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: if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None:
cg.add(var.set_accuracy_decimals(accuracy_decimals)) cg.add(var.set_accuracy_decimals(accuracy_decimals))
# Only set force_update if True (default is False) # 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. * 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: public:
explicit Sensor(); explicit Sensor();
+2 -2
View File
@@ -567,7 +567,7 @@ void Sprinkler::set_valve_run_duration(const optional<size_t> valve_number, cons
return; return;
} }
auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); 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); call.set_value(run_duration.value() / 60.0);
} else { } else {
call.set_value(run_duration.value()); call.set_value(run_duration.value());
@@ -649,7 +649,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
return 0; return 0;
} }
if (this->valve_[valve_number].run_duration_number != nullptr) { 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)); return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state * 60));
} else { } else {
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state)); 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, DEVICE_CLASS_SWITCH,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -154,9 +158,8 @@ async def _build_switch_automations(var, config):
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
@setup_entity("switch")
async def setup_switch_core_(var, config): async def setup_switch_core_(var, config):
await setup_entity(var, config, "switch")
if (inverted := config.get(CONF_INVERTED)) is not None: if (inverted := config.get(CONF_INVERTED)) is not None:
cg.add(var.set_inverted(inverted)) 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): if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config) await web_server.add_entity_config(var, web_server_config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: setup_device_class(config)
cg.add(var.set_device_class(device_class))
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
await zigbee.setup_switch(var, config) 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) * 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. * and a write_state method that writes a state to the hardware.
*/ */
class Switch : public EntityBase, public EntityBase_DeviceClass { class Switch : public EntityBase {
public: public:
explicit Switch(); explicit Switch();
+1 -2
View File
@@ -84,6 +84,7 @@ def text_schema(
return _TEXT_SCHEMA.extend(schema) return _TEXT_SCHEMA.extend(schema)
@setup_entity("text")
async def setup_text_core_( async def setup_text_core_(
var, var,
config, config,
@@ -92,8 +93,6 @@ async def setup_text_core_(
max_length: int | None, max_length: int | None,
pattern: str | None, pattern: str | None,
): ):
await setup_entity(var, config, "text")
cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_min_length(min_length))
cg.add(var.traits.set_max_length(max_length)) cg.add(var.traits.set_max_length(max_length))
if pattern is not None: if pattern is not None:
+7 -5
View File
@@ -21,7 +21,11 @@ from esphome.const import (
DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_TIMESTAMP,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
from esphome.util import Registry 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) await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
@setup_entity("text_sensor")
async def setup_text_sensor_core_(var, config): async def setup_text_sensor_core_(var, config):
await setup_entity(var, config, "text_sensor") setup_device_class(config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
if config.get(CONF_FILTERS): # must exist and not be empty if config.get(CONF_FILTERS): # must exist and not be empty
cg.add_define("USE_TEXT_SENSOR_FILTER") 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: \ public: \
void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } 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: public:
std::string state; std::string state;
+7 -5
View File
@@ -15,7 +15,11 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_CONFIG,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
@@ -87,11 +91,9 @@ def update_schema(
return _UPDATE_SCHEMA.extend(schema) return _UPDATE_SCHEMA.extend(schema)
@setup_entity("update")
async def setup_update_core_(var, config): async def setup_update_core_(var, config):
await setup_entity(var, config, "update") setup_device_class(config)
if device_class_config := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class_config))
if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE): if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE):
await automation.build_automation( await automation.build_automation(
+1 -1
View File
@@ -29,7 +29,7 @@ enum UpdateState : uint8_t {
const LogString *update_state_to_string(UpdateState state); const LogString *update_state_to_string(UpdateState state);
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { class UpdateEntity : public EntityBase {
public: public:
void publish_state(); void publish_state();
+7 -5
View File
@@ -22,7 +22,11 @@ from esphome.const import (
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority 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.cpp_generator import MockObjClass
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -129,11 +133,9 @@ def valve_schema(
return _VALVE_SCHEMA.extend(schema) return _VALVE_SCHEMA.extend(schema)
@setup_entity("valve")
async def _setup_valve_core(var, config): async def _setup_valve_core(var, config):
await setup_entity(var, config, "valve") setup_device_class(config)
if device_class_config := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class_config))
for conf in config.get(CONF_ON_OPEN, []): for conf in config.get(CONF_ON_OPEN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 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 * to control all values of the valve. Also implement get_traits() to return what operations
* the valve supports. * the valve supports.
*/ */
class Valve : public EntityBase, public EntityBase_DeviceClass { class Valve : public EntityBase {
public: public:
explicit Valve(); explicit Valve();
+1 -2
View File
@@ -69,10 +69,9 @@ def water_heater_schema(
return _WATER_HEATER_SCHEMA.extend(schema) return _WATER_HEATER_SCHEMA.extend(schema)
@setup_entity("water_heater")
async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None: async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up the core water heater properties in C++.""" """Set up the core water heater properties in C++."""
await setup_entity(var, config, "water_heater")
visual = config[CONF_VISUAL] visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") 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; json::JsonBuilder builder;
JsonObject root = builder.root(); 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()); const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
// Need two buffers: one for value, one for state with UOM // 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_cycle_count() { return k_cycle_get_32(); }
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } 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; } 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; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
Mutex::Mutex() { Mutex::Mutex() {
+2
View File
@@ -44,7 +44,9 @@
#define USE_DEEP_SLEEP #define USE_DEEP_SLEEP
#define USE_DEVICES #define USE_DEVICES
#define USE_DISPLAY #define USE_DISPLAY
#define USE_ENTITY_DEVICE_CLASS
#define USE_ENTITY_ICON #define USE_ENTITY_ICON
#define USE_ENTITY_UNIT_OF_MEASUREMENT
#define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_CAMERA_JPEG_CONVERSION
#define USE_ESP32_HOSTED #define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK #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 // Weak default lookup functions — overridden by generated code in main.cpp
std::string EntityBase::get_icon() const { __attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return ""; }
#ifdef USE_ENTITY_ICON __attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; }
if (this->icon_c_str_ == nullptr) { __attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; }
return "";
} // Entity device class (from index)
return this->icon_c_str_; StringRef EntityBase::get_device_class_ref() const {
#ifdef USE_ENTITY_DEVICE_CLASS
return StringRef(entity_device_class_lookup(this->device_class_idx_));
#else #else
return ""; return StringRef(entity_device_class_lookup(0));
#endif #endif
} }
void EntityBase::set_icon(const char *icon) { std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); }
#ifdef USE_ENTITY_ICON
this->icon_c_str_ = icon; // 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 #else
// No-op when USE_ENTITY_ICON is not defined return StringRef(entity_uom_lookup(0));
#endif #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 // Entity Object ID - computed on-demand from name
std::string EntityBase::get_object_id() const { 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); 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 #ifdef USE_ENTITY_ICON
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
if (!obj.get_icon_ref().empty()) { if (!obj.get_icon_ref().empty()) {
@@ -160,13 +160,13 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj)
} }
#endif #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()) { if (!obj.get_device_class_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); 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()) { 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()); 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 { 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 // 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; 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); 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 // Get/set this entity's icon
ESPDEPRECATED( ESPDEPRECATED(
"Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0",
"2025.11.0") "2025.11.0")
std::string get_icon() const; std::string get_icon() const;
void set_icon(const char *icon); StringRef get_icon_ref() const;
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
}
#ifdef USE_DEVICES #ifdef USE_DEVICES
// Get/set this entity's device id // Get/set this entity's device id
@@ -173,9 +200,6 @@ class EntityBase {
void calc_object_id_(); void calc_object_id_();
StringRef name_; StringRef name_;
#ifdef USE_ENTITY_ICON
const char *icon_c_str_{nullptr};
#endif
uint32_t object_id_hash_{}; uint32_t object_id_hash_{};
#ifdef USE_DEVICES #ifdef USE_DEVICES
Device *device_{}; Device *device_{};
@@ -190,44 +214,16 @@ class EntityBase {
uint8_t entity_category : 2; // Supports up to 4 categories uint8_t entity_category : 2; // Supports up to 4 categories
uint8_t reserved : 2; // Reserved for future use uint8_t reserved : 2; // Reserved for future use
} flags_{}; } flags_{};
}; // String table indices — packed into the 3 padding bytes after flags_
#ifdef USE_ENTITY_DEVICE_CLASS
class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) uint8_t device_class_idx_{};
public: #endif
/// Get the device class, using the manual override if set. #ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " uint8_t uom_idx_{};
"ESPHome 2026.5.0", #endif
"2025.11.0") #ifdef USE_ENTITY_ICON
std::string get_device_class(); uint8_t icon_idx_{};
/// Manually set the device class. #endif
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
}; };
/// Log entity icon if set (for use in dump_config) /// 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 #endif
/// Log entity device class if set (for use in dump_config) /// 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) #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) /// 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) #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. * An entity that has a state.
+221 -6
View File
@@ -1,9 +1,12 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field
import functools
import logging import logging
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DISABLED_BY_DEFAULT, CONF_DISABLED_BY_DEFAULT,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@@ -11,15 +14,184 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_INTERNAL, CONF_INTERNAL,
CONF_NAME, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
) )
from esphome.core import CORE, ID from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, add, get_variable from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
import esphome.final_validate as fv 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 from esphome.types import ConfigType, EntityMetadata
_LOGGER = logging.getLogger(__name__) _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( def get_base_entity_object_id(
name: str, friendly_name: str | None, device_name: str | None = None 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)) return sanitize(snake_case(base_str))
async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: def setup_entity(var_or_platform, config=None, platform=None):
"""Set up generic properties of an Entity. """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, This function sets up the common entity properties like name, icon,
entity category, etc. 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)) add(var.set_disabled_by_default(True))
if CONF_INTERNAL in config: if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL])) add(var.set_internal(config[CONF_INTERNAL]))
icon_idx = 0
if CONF_ICON in config: if CONF_ICON in config:
# Add USE_ENTITY_ICON define when icons are used # Add USE_ENTITY_ICON define when icons are used
cg.add_define("USE_ENTITY_ICON") 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: if CONF_ENTITY_CATEGORY in config:
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) 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): 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_cycle_count();
uint32_t arch_get_cpu_freq_hz(); uint32_t arch_get_cpu_freq_hz();
uint8_t progmem_read_byte(const uint8_t *addr); 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); uint16_t progmem_read_uint16(const uint16_t *addr);
} // namespace esphome } // namespace esphome
+1
View File
@@ -79,6 +79,7 @@ def clang_options(idedata):
"-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))",
"-Dpgm_read_word(s)=(*(const uint16_t *)(s))", "-Dpgm_read_word(s)=(*(const uint16_t *)(s))",
"-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))",
"-Dpgm_read_ptr(s)=(*(const void *const *)(s))",
"-DPROGMEM=", "-DPROGMEM=",
"-DPGM_P=const char *", "-DPGM_P=const char *",
"-DPSTR(s)=(s)", "-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") main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
# Then # 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") main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then # Then
assert 'ts_2->set_device_class("timestamp");' in main_cpp assert "ts_2->set_entity_strings(" in main_cpp
assert 'ts_3->set_device_class("date");' 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