pack_entity_strings

This commit is contained in:
J. Nick Koston
2026-02-20 22:22:48 -06:00
parent 0e38acd67a
commit 4eac632939
52 changed files with 473 additions and 206 deletions

View File

@@ -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)

View File

@@ -770,9 +770,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();

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
@@ -550,11 +554,9 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema)
@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
)

View File

@@ -28,7 +28,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(){};

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)

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.
*

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")

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(

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();

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)

View File

@@ -47,6 +47,7 @@ void arch_init() {
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_word(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() {
uint32_t freq = 0;

View File

@@ -368,11 +368,16 @@ SETTERS = {
}
@setup_entity("camera")
async def _setup_esp32_camera(var, config):
pass
async def to_code(config):
cg.add_define("USE_CAMERA")
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config, "camera")
await _setup_esp32_camera(var, config)
await cg.register_component(var, config)
for key, setter in SETTERS.items():

View File

@@ -32,6 +32,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
}
uint16_t progmem_read_word(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }

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)

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);

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:

View File

@@ -53,6 +53,7 @@ void HOT arch_feed_wdt() {
}
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_word(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);

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:

View File

@@ -34,6 +34,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; }
uint16_t progmem_read_word(const uint16_t *addr) { return *addr; }
} // namespace esphome

View File

@@ -208,9 +208,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:
@@ -274,7 +273,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):

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)

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)

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;
}

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"]
@@ -240,11 +245,10 @@ def number_schema(
return _NUMBER_SCHEMA.extend(schema)
@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))
@@ -268,10 +272,8 @@ async def setup_number_core_(
cg.add(trigger.set_max(template_))
await automation.build_automation(trigger, [(float, "x")], conf)
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)

View File

@@ -1,4 +1,5 @@
#include "number.h"
#include <cstddef>
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
@@ -15,8 +16,20 @@ 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);
}
// Deprecated backward-compat: delegate to parent Number's EntityBase methods
StringRef NumberTraits::get_device_class_ref() const {
const auto *number =
reinterpret_cast<const Number *>(reinterpret_cast<const uint8_t *>(this) - offsetof(Number, traits));
return number->get_device_class_ref();
}
StringRef NumberTraits::get_unit_of_measurement_ref() const {
const auto *number =
reinterpret_cast<const Number *>(reinterpret_cast<const uint8_t *>(this) - offsetof(Number, traits));
return number->get_unit_of_measurement_ref();
}
void Number::publish_state(float state) {

View File

@@ -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; }
@@ -27,6 +27,16 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas
void set_mode(NumberMode mode) { this->mode_ = mode; }
NumberMode get_mode() const { return this->mode_; }
// Deprecated: use Number::get_device_class_ref() instead.
// Delegates to parent Number's EntityBase via offsetof.
ESPDEPRECATED("Use number->get_device_class_ref() instead. Removed in 2027.2.0", "2026.8.0")
StringRef get_device_class_ref() const;
// Deprecated: use Number::get_unit_of_measurement_ref() instead.
// Delegates to parent Number's EntityBase via offsetof.
ESPDEPRECATED("Use number->get_unit_of_measurement_ref() instead. Removed in 2027.2.0", "2026.8.0")
StringRef get_unit_of_measurement_ref() const;
protected:
float min_value_ = NAN;
float max_value_ = NAN;

View File

@@ -32,6 +32,9 @@ void HOT arch_feed_wdt() { watchdog_update(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint16_t progmem_read_word(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }

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, []):

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
@@ -888,15 +893,12 @@ async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)
@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)

View File

@@ -40,7 +40,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();

View File

@@ -566,7 +566,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());
@@ -648,7 +648,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));

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"]
@@ -141,9 +145,8 @@ def switch_schema(
return _SWITCH_SCHEMA.extend(schema)
@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))
for conf in config.get(CONF_ON_STATE, []):
@@ -163,8 +166,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)

View File

@@ -36,7 +36,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();

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:

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
@@ -197,11 +201,9 @@ async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)
@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
filters = await build_filters(config[CONF_FILTERS])

View File

@@ -22,7 +22,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;

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(

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();

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)

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();

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")

View File

@@ -1131,7 +1131,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

View File

@@ -59,6 +59,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; }
uint16_t progmem_read_word(const uint16_t *addr) { return *addr; }
Mutex::Mutex() {
auto *mutex = new k_mutex();

View File

@@ -42,7 +42,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

View File

@@ -45,24 +45,46 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
}
}
// Entity Icon
std::string EntityBase::get_icon() const {
// Weak default lookup functions — overridden by generated code in main.cpp
__attribute__((weak)) const char *entity_device_class_lookup(uint16_t) { return ""; }
__attribute__((weak)) const char *entity_uom_lookup(uint16_t) { return ""; }
__attribute__((weak)) const char *entity_icon_lookup(uint16_t) { return ""; }
// Entity device class (from packed index)
StringRef EntityBase::get_device_class_ref() const {
static constexpr auto EMPTY = StringRef::from_lit("");
uint16_t idx = (this->entity_string_packed_ >> ENTITY_STR_DC_SHIFT) & ENTITY_STR_DC_MASK;
if (idx == 0)
return EMPTY;
return StringRef(entity_device_class_lookup(idx));
}
std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); }
// Entity unit of measurement (from packed index)
StringRef EntityBase::get_unit_of_measurement_ref() const {
static constexpr auto EMPTY = StringRef::from_lit("");
uint16_t idx = (this->entity_string_packed_ >> ENTITY_STR_UOM_SHIFT) & ENTITY_STR_UOM_MASK;
if (idx == 0)
return EMPTY;
return StringRef(entity_uom_lookup(idx));
}
std::string EntityBase::get_unit_of_measurement() const {
return std::string(this->get_unit_of_measurement_ref().c_str());
}
// Entity icon (from packed index)
StringRef EntityBase::get_icon_ref() const {
static constexpr auto EMPTY = StringRef::from_lit("");
#ifdef USE_ENTITY_ICON
if (this->icon_c_str_ == nullptr) {
return "";
}
return this->icon_c_str_;
uint16_t idx = (this->entity_string_packed_ >> ENTITY_STR_ICON_SHIFT) & ENTITY_STR_ICON_MASK;
if (idx == 0)
return EMPTY;
return StringRef(entity_icon_lookup(idx));
#else
return "";
#endif
}
void EntityBase::set_icon(const char *icon) {
#ifdef USE_ENTITY_ICON
this->icon_c_str_ = icon;
#else
// No-op when USE_ENTITY_ICON is not defined
return EMPTY;
#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 +156,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 +164,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());
}

View File

@@ -14,6 +14,21 @@
namespace esphome {
// Extern lookup functions for packed entity string tables.
// Generated code provides strong definitions; weak defaults return "".
extern const char *entity_device_class_lookup(uint16_t index);
extern const char *entity_uom_lookup(uint16_t index);
extern const char *entity_icon_lookup(uint16_t index);
// Bit layout for entity_string_packed_:
// [31..20] icon (12 bits) | [19..10] UoM (10 bits) | [9..0] device_class (10 bits)
static constexpr uint8_t ENTITY_STR_DC_SHIFT = 0;
static constexpr uint8_t ENTITY_STR_UOM_SHIFT = 10;
static constexpr uint8_t ENTITY_STR_ICON_SHIFT = 20;
static constexpr uint16_t ENTITY_STR_DC_MASK = 0x3FF;
static constexpr uint16_t ENTITY_STR_UOM_MASK = 0x3FF;
static constexpr uint16_t ENTITY_STR_ICON_MASK = 0xFFF;
// 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 +104,31 @@ class EntityBase {
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
}
// Set packed entity string indices — one call per entity from codegen.
// Bit layout: [31..20] icon (12 bits) | [19..10] UoM (10 bits) | [9..0] device_class (10 bits)
void set_entity_strings(uint32_t packed) { this->entity_string_packed_ = packed; }
// 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 +199,7 @@ class EntityBase {
void calc_object_id_();
StringRef name_;
#ifdef USE_ENTITY_ICON
const char *icon_c_str_{nullptr};
#endif
uint32_t entity_string_packed_{0}; // bits 0-9: device_class, 10-19: uom, 20-31: icon
uint32_t object_id_hash_{};
#ifdef USE_DEVICES
Device *device_{};
@@ -192,42 +216,14 @@ class EntityBase {
} flags_{};
};
// Empty shell — methods moved to EntityBase. Kept for backward compatibility.
// TODO: Remove in 2027.2.0
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
};
// Empty shell — methods moved to EntityBase. Kept for backward compatibility.
// TODO: Remove in 2027.2.0
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)
@@ -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.

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.cpp_generator import DeferredStatement, MockObj, 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"
@dataclass
class EntityStringPool:
"""Pool of entity string properties packed into contiguous blobs.
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++ blob + PROGMEM offset 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:
"""Register the deferred global statement for table generation (once)."""
pool = _get_pool()
if pool._tables_registered:
return
pool._tables_registered = True
cg.add_global(DeferredStatement(_generate_tables))
def _generate_blob_and_offsets(
strings: dict[str, int],
) -> tuple[bytes, list[int]]:
"""Build a packed blob and offset list from a string dict.
Returns (blob_bytes, offsets) where offsets[0] points to "" (empty)
and offsets[i] points to the i-th string.
"""
# Sort by assigned index to ensure deterministic output
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
blob = bytearray(b"\x00") # Index 0 = offset 0 = empty string
offsets = [0] # offset for index 0 (empty string)
for s, _idx in sorted_strings:
offsets.append(len(blob))
blob.extend(s.encode("utf-8"))
blob.append(0) # null terminator
return bytes(blob), offsets
def _generate_category_code(
blob_var: str,
offsets_var: str,
lookup_fn: str,
strings: dict[str, int],
) -> str:
"""Generate C++ code for one string category (blob + offsets + lookup)."""
if not strings:
return ""
blob_bytes, offsets = _generate_blob_and_offsets(strings)
use_uint16 = len(blob_bytes) > 255
offset_type = "uint16_t" if use_uint16 else "uint8_t"
read_fn = "progmem_read_word" if use_uint16 else "progmem_read_byte"
count = len(offsets)
blob_escaped = cpp_string_escape(blob_bytes)
offsets_str = ", ".join(str(o) for o in offsets)
return (
f"static const char {blob_var}[] = {blob_escaped};\n"
f"static const {offset_type} {offsets_var}[] PROGMEM = {{{offsets_str}}};\n"
f"const char *{lookup_fn}(uint16_t index) {{\n"
f" if (index >= {count}) return {blob_var};\n"
f" return &{blob_var}[{read_fn}(&{offsets_var}[index])];\n"
f"}}\n"
)
_CATEGORY_CONFIGS = (
("ENTITY_DC", "entity_device_class_lookup", "device_classes"),
("ENTITY_UOM", "entity_uom_lookup", "units"),
("ENTITY_ICON", "entity_icon_lookup", "icons"),
)
def _generate_tables() -> str:
"""Generate all entity string table C++ code. Called at render time."""
pool = _get_pool()
parts = ["namespace esphome {"]
for prefix, lookup_fn, attr in _CATEGORY_CONFIGS:
code = _generate_category_code(
f"{prefix}_BLOB", f"{prefix}_OFFSETS", lookup_fn, getattr(pool, attr)
)
if code:
parts.append(code)
parts.append("} // namespace esphome")
return "\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, 1023, "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, 1023, "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, 4095, "icon")
def setup_device_class(config: ConfigType) -> None:
"""Register config's device_class and store its index for finalize_entity_strings."""
config[_KEY_DC_IDX] = register_device_class(config.get(CONF_DEVICE_CLASS, ""))
def setup_unit_of_measurement(config: ConfigType) -> None:
"""Register config's unit_of_measurement and store its index for finalize_entity_strings."""
config[_KEY_UOM_IDX] = register_unit_of_measurement(
config.get(CONF_UNIT_OF_MEASUREMENT, "")
)
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 | (uom_idx << 10) | (icon_idx << 20)
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,16 +236,40 @@ 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(platform: str) -> Callable:
"""Decorator for component setup functions.
Wraps the function to:
1. Set up common entity properties (name, icon, etc.)
2. Run the wrapped function (which may call setup_device_class, etc.)
3. Finalize entity strings (pack dc/uom/icon indices into uint32_t)
Usage::
@setup_entity("sensor")
async def setup_sensor_core_(var, config):
setup_device_class(config)
setup_unit_of_measurement(config)
...
"""
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
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.
Args:
var: The entity variable to set up
config: Configuration dictionary containing entity settings
platform: The platform name (e.g., "sensor", "binary_sensor")
"""
# Get device info if configured
if device_id_obj := config.get(CONF_DEVICE_ID):
@@ -92,12 +288,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):

View File

@@ -41,5 +41,6 @@ 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);
uint16_t progmem_read_word(const uint16_t *addr);
} // namespace esphome

View File

@@ -393,6 +393,23 @@ class RawStatement(Statement):
return self.text
class DeferredStatement(Statement):
"""Statement evaluated lazily at render time (after all to_code() calls).
Use this to generate code that depends on state accumulated across
multiple components' to_code() calls. The resolver callback is only
called when the statement is converted to a string during code generation.
"""
__slots__ = ("_resolver",)
def __init__(self, resolver: Callable[[], str]):
self._resolver = resolver
def __str__(self):
return self._resolver()
class ExpressionStatement(Statement):
__slots__ = ("expression",)

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

View File

@@ -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

View File

@@ -18,9 +18,9 @@ from esphome.const import (
)
from esphome.core import CORE, ID, entity_helpers
from esphome.core.entity_helpers import (
_setup_entity_impl,
entity_duplicate_validator,
get_base_entity_object_id,
setup_entity,
)
from esphome.cpp_generator import MockObj
from esphome.helpers import sanitize, snake_case
@@ -305,7 +305,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) ->
CONF_NAME: "Temperature",
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var1, config1, "sensor")
await _setup_entity_impl(var1, config1, "sensor")
# Get object ID from first entity
object_id1 = extract_object_id_from_expressions(added_expressions)
@@ -319,7 +319,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) ->
CONF_NAME: "Humidity",
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var2, config2, "sensor")
await _setup_entity_impl(var2, config2, "sensor")
# Get object ID from second entity
object_id2 = extract_object_id_from_expressions(added_expressions)
@@ -354,7 +354,7 @@ async def test_setup_entity_different_platforms(
object_ids: list[str] = []
for var, platform in platforms:
added_expressions.clear()
await setup_entity(var, config, platform)
await _setup_entity_impl(var, config, platform)
object_id = extract_object_id_from_expressions(added_expressions)
object_ids.append(object_id)
@@ -416,7 +416,7 @@ async def test_setup_entity_with_devices(
object_ids: list[str] = []
for var, config in [(sensor1, config1), (sensor2, config2)]:
added_expressions.clear()
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
object_id = extract_object_id_from_expressions(added_expressions)
object_ids.append(object_id)
@@ -438,7 +438,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
object_id = extract_object_id_from_expressions(added_expressions)
# Should use friendly name
@@ -460,7 +460,7 @@ async def test_setup_entity_special_characters(
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
object_id = extract_object_id_from_expressions(added_expressions)
# Special characters should be sanitized
@@ -471,7 +471,7 @@ async def test_setup_entity_special_characters(
async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None:
"""Test setup_entity sets icon correctly."""
added_expressions = setup_test_environment
setup_test_environment # noqa: F841 - fixture initializes CORE state
var = MockObj("sensor1")
@@ -481,12 +481,10 @@ async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None
CONF_ICON: "mdi:thermometer",
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
# Check icon was set
assert any(
'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions
)
# Check icon index was stored in config for finalize_entity_strings
assert config.get("_entity_icon_idx", 0) > 0
@pytest.mark.asyncio
@@ -504,7 +502,7 @@ async def test_setup_entity_disabled_by_default(
CONF_DISABLED_BY_DEFAULT: True,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
# Check disabled_by_default was set
assert any(
@@ -790,7 +788,7 @@ async def test_setup_entity_empty_name_with_device(
CONF_DEVICE_ID: device_id,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
entity_helpers.get_variable = original_get_variable
@@ -826,7 +824,7 @@ async def test_setup_entity_empty_name_with_mac_suffix(
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
assert any('set_name("", 0)' in expr for expr in added_expressions), (
@@ -858,7 +856,7 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name(
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
assert any('set_name("", 0)' in expr for expr in added_expressions), (
@@ -891,7 +889,7 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name(
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
await _setup_entity_impl(var, config, "sensor")
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
assert any('set_name("", 0)' in expr for expr in added_expressions), (