mirror of
https://github.com/esphome/esphome.git
synced 2026-02-24 20:35:30 -07:00
pack_entity_strings
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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(){};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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, []):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",)
|
||||
|
||||
|
||||
@@ -11,4 +11,4 @@ def test_sensor_device_class_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 's_1->set_device_class("voltage");' in main_cpp
|
||||
assert "s_1->set_entity_strings(" in main_cpp
|
||||
|
||||
@@ -54,5 +54,5 @@ def test_text_sensor_device_class_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_2->set_device_class("timestamp");' in main_cpp
|
||||
assert 'ts_3->set_device_class("date");' in main_cpp
|
||||
assert "ts_2->set_entity_strings(" in main_cpp
|
||||
assert "ts_3->set_entity_strings(" in main_cpp
|
||||
|
||||
@@ -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), (
|
||||
|
||||
Reference in New Issue
Block a user