mirror of
https://github.com/esphome/esphome.git
synced 2026-05-23 03:06:05 +08:00
[core] Pack entity string properties into PROGMEM-indexed uint8_t fields (#14171)
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)
|
||||
|
||||
@@ -773,9 +773,9 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
|
||||
uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *number = static_cast<number::Number *>(entity);
|
||||
ListEntitiesNumberResponse msg;
|
||||
msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref();
|
||||
msg.unit_of_measurement = number->get_unit_of_measurement_ref();
|
||||
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
|
||||
msg.device_class = number->traits.get_device_class_ref();
|
||||
msg.device_class = number->get_device_class_ref();
|
||||
msg.min_value = number->traits.get_min_value();
|
||||
msg.max_value = number->traits.get_max_value();
|
||||
msg.step = number->traits.get_step();
|
||||
|
||||
@@ -60,7 +60,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.util import Registry
|
||||
|
||||
@@ -604,11 +608,9 @@ async def _build_binary_sensor_automations(var, config):
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("binary_sensor")
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
||||
* The sub classes should notify the front-end of new states via the publish_state() method which
|
||||
* handles inverted inputs for you.
|
||||
*/
|
||||
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
|
||||
class BinarySensor : public StatefulEntityBase<bool> {
|
||||
public:
|
||||
explicit BinarySensor(){};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -48,6 +48,7 @@ void arch_init() {
|
||||
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
|
||||
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
const char *progmem_read_ptr(const char *const *addr) {
|
||||
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
|
||||
}
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) {
|
||||
return pgm_read_word(addr); // NOLINT
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -59,6 +59,7 @@ void HOT arch_feed_wdt() {
|
||||
}
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); }
|
||||
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -243,9 +243,8 @@ def validate_color_temperature_channels(value):
|
||||
return value
|
||||
|
||||
|
||||
async def setup_light_core_(light_var, output_var, config):
|
||||
await setup_entity(light_var, config, "light")
|
||||
|
||||
@setup_entity("light")
|
||||
async def setup_light_core_(light_var, config, output_var):
|
||||
cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
|
||||
if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None:
|
||||
@@ -312,7 +311,7 @@ async def register_light(output_var, config):
|
||||
cg.add(cg.App.register_light(light_var))
|
||||
CORE.register_platform_component("light", light_var)
|
||||
await cg.register_component(light_var, config)
|
||||
await setup_light_core_(light_var, output_var, config)
|
||||
await setup_light_core_(light_var, config, output_var)
|
||||
|
||||
|
||||
async def new_light(config, *args):
|
||||
|
||||
@@ -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"]
|
||||
@@ -257,11 +262,10 @@ async def _build_number_automations(var, config):
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
|
||||
|
||||
@setup_entity("number")
|
||||
async def setup_number_core_(
|
||||
var, config, *, min_value: float, max_value: float, step: float
|
||||
):
|
||||
await setup_entity(var, config, "number")
|
||||
|
||||
cg.add(var.traits.set_min_value(min_value))
|
||||
cg.add(var.traits.set_max_value(max_value))
|
||||
cg.add(var.traits.set_step(step))
|
||||
@@ -273,10 +277,8 @@ async def setup_number_core_(
|
||||
|
||||
CORE.add_job(_build_number_automations, var, config)
|
||||
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.traits.set_unit_of_measurement(unit_of_measurement))
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.traits.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
setup_unit_of_measurement(config)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -15,8 +15,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
LOG_ENTITY_ICON(tag, prefix, *obj);
|
||||
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits);
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits);
|
||||
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, *obj);
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
void Number::publish_state(float state) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::number {
|
||||
|
||||
@@ -11,7 +11,7 @@ enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_SLIDER = 2,
|
||||
};
|
||||
|
||||
class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement {
|
||||
class NumberTraits {
|
||||
public:
|
||||
// Set/get the number value boundaries.
|
||||
void set_min_value(float min_value) { min_value_ = min_value; }
|
||||
|
||||
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { watchdog_update(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
const char *progmem_read_ptr(const char *const *addr) {
|
||||
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
|
||||
}
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -908,15 +913,12 @@ async def _build_sensor_automations(var, config):
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
|
||||
|
||||
@setup_entity("sensor")
|
||||
async def setup_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
setup_unit_of_measurement(config)
|
||||
if (state_class := config.get(CONF_STATE_CLASS)) is not None:
|
||||
cg.add(var.set_state_class(state_class))
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.set_unit_of_measurement(unit_of_measurement))
|
||||
if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None:
|
||||
cg.add(var.set_accuracy_decimals(accuracy_decimals))
|
||||
# Only set force_update if True (default is False)
|
||||
|
||||
@@ -44,7 +44,7 @@ const LogString *state_class_to_string(StateClass state_class);
|
||||
*
|
||||
* A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy.
|
||||
*/
|
||||
class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement {
|
||||
class Sensor : public EntityBase {
|
||||
public:
|
||||
explicit Sensor();
|
||||
|
||||
|
||||
@@ -567,7 +567,7 @@ void Sprinkler::set_valve_run_duration(const optional<size_t> valve_number, cons
|
||||
return;
|
||||
}
|
||||
auto call = this->valve_[valve_number.value()].run_duration_number->make_call();
|
||||
if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
|
||||
if (this->valve_[valve_number.value()].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
|
||||
call.set_value(run_duration.value() / 60.0);
|
||||
} else {
|
||||
call.set_value(run_duration.value());
|
||||
@@ -649,7 +649,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
|
||||
return 0;
|
||||
}
|
||||
if (this->valve_[valve_number].run_duration_number != nullptr) {
|
||||
if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
|
||||
if (this->valve_[valve_number].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
|
||||
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state * 60));
|
||||
} else {
|
||||
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state));
|
||||
|
||||
@@ -22,7 +22,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_SWITCH,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -154,9 +158,8 @@ async def _build_switch_automations(var, config):
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
@setup_entity("switch")
|
||||
async def setup_switch_core_(var, config):
|
||||
await setup_entity(var, config, "switch")
|
||||
|
||||
if (inverted := config.get(CONF_INVERTED)) is not None:
|
||||
cg.add(var.set_inverted(inverted))
|
||||
|
||||
@@ -169,8 +172,7 @@ async def setup_switch_core_(var, config):
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
await zigbee.setup_switch(var, config)
|
||||
|
||||
@@ -35,7 +35,7 @@ enum SwitchRestoreMode : uint8_t {
|
||||
* A switch is basically just a combination of a binary sensor (for reporting switch values)
|
||||
* and a write_state method that writes a state to the hardware.
|
||||
*/
|
||||
class Switch : public EntityBase, public EntityBase_DeviceClass {
|
||||
class Switch : public EntityBase {
|
||||
public:
|
||||
explicit Switch();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -208,11 +212,9 @@ async def _build_text_sensor_automations(var, config):
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
|
||||
@setup_entity("text_sensor")
|
||||
async def setup_text_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "text_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
if config.get(CONF_FILTERS): # must exist and not be empty
|
||||
cg.add_define("USE_TEXT_SENSOR_FILTER")
|
||||
|
||||
@@ -25,7 +25,7 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
|
||||
public: \
|
||||
void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; }
|
||||
|
||||
class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
class TextSensor : public EntityBase {
|
||||
public:
|
||||
std::string state;
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1139,7 +1139,7 @@ json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float v
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
|
||||
const auto uom_ref = obj->get_unit_of_measurement_ref();
|
||||
const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
|
||||
|
||||
// Need two buffers: one for value, one for state with UOM
|
||||
|
||||
@@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
|
||||
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
|
||||
Mutex::Mutex() {
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
#define USE_DEEP_SLEEP
|
||||
#define USE_DEVICES
|
||||
#define USE_DISPLAY
|
||||
#define USE_ENTITY_DEVICE_CLASS
|
||||
#define USE_ENTITY_ICON
|
||||
#define USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
#define USE_ESP32_CAMERA_JPEG_CONVERSION
|
||||
#define USE_ESP32_HOSTED
|
||||
#define USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
|
||||
@@ -45,24 +45,42 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
|
||||
}
|
||||
}
|
||||
|
||||
// Entity Icon
|
||||
std::string EntityBase::get_icon() const {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
if (this->icon_c_str_ == nullptr) {
|
||||
return "";
|
||||
}
|
||||
return this->icon_c_str_;
|
||||
// Weak default lookup functions — overridden by generated code in main.cpp
|
||||
__attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return ""; }
|
||||
__attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; }
|
||||
__attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; }
|
||||
|
||||
// Entity device class (from index)
|
||||
StringRef EntityBase::get_device_class_ref() const {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
return StringRef(entity_device_class_lookup(this->device_class_idx_));
|
||||
#else
|
||||
return "";
|
||||
return StringRef(entity_device_class_lookup(0));
|
||||
#endif
|
||||
}
|
||||
void EntityBase::set_icon(const char *icon) {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
this->icon_c_str_ = icon;
|
||||
std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); }
|
||||
|
||||
// Entity unit of measurement (from index)
|
||||
StringRef EntityBase::get_unit_of_measurement_ref() const {
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
return StringRef(entity_uom_lookup(this->uom_idx_));
|
||||
#else
|
||||
// No-op when USE_ENTITY_ICON is not defined
|
||||
return StringRef(entity_uom_lookup(0));
|
||||
#endif
|
||||
}
|
||||
std::string EntityBase::get_unit_of_measurement() const {
|
||||
return std::string(this->get_unit_of_measurement_ref().c_str());
|
||||
}
|
||||
|
||||
// Entity icon (from index)
|
||||
StringRef EntityBase::get_icon_ref() const {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
return StringRef(entity_icon_lookup(this->icon_idx_));
|
||||
#else
|
||||
return StringRef(entity_icon_lookup(0));
|
||||
#endif
|
||||
}
|
||||
std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); }
|
||||
|
||||
// Entity Object ID - computed on-demand from name
|
||||
std::string EntityBase::get_object_id() const {
|
||||
@@ -134,24 +152,6 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve
|
||||
return global_preferences->make_preference(size, key);
|
||||
}
|
||||
|
||||
std::string EntityBase_DeviceClass::get_device_class() {
|
||||
if (this->device_class_ == nullptr) {
|
||||
return "";
|
||||
}
|
||||
return this->device_class_;
|
||||
}
|
||||
|
||||
void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; }
|
||||
|
||||
std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() {
|
||||
if (this->unit_of_measurement_ == nullptr)
|
||||
return "";
|
||||
return this->unit_of_measurement_;
|
||||
}
|
||||
void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_measurement) {
|
||||
this->unit_of_measurement_ = unit_of_measurement;
|
||||
}
|
||||
|
||||
#ifdef USE_ENTITY_ICON
|
||||
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_icon_ref().empty()) {
|
||||
@@ -160,13 +160,13 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj)
|
||||
}
|
||||
#endif
|
||||
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_device_class_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj) {
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_unit_of_measurement_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj.get_unit_of_measurement_ref().c_str());
|
||||
}
|
||||
|
||||
+48
-52
@@ -14,6 +14,12 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Extern lookup functions for entity string tables.
|
||||
// Generated code provides strong definitions; weak defaults return "".
|
||||
extern const char *entity_device_class_lookup(uint8_t index);
|
||||
extern const char *entity_uom_lookup(uint8_t index);
|
||||
extern const char *entity_icon_lookup(uint8_t index);
|
||||
|
||||
// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py
|
||||
static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31;
|
||||
|
||||
@@ -89,20 +95,41 @@ class EntityBase {
|
||||
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
|
||||
}
|
||||
|
||||
// Set entity string table indices — one call per entity from codegen.
|
||||
// Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits)
|
||||
void set_entity_strings([[maybe_unused]] uint32_t packed) {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
this->device_class_idx_ = packed & 0xFF;
|
||||
#endif
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
this->uom_idx_ = (packed >> 8) & 0xFF;
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
this->icon_idx_ = (packed >> 16) & 0xFF;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get device class as StringRef (from packed index)
|
||||
StringRef get_device_class_ref() const;
|
||||
/// Get the device class as std::string (deprecated, prefer get_device_class_ref())
|
||||
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in "
|
||||
"ESPHome 2026.9.0",
|
||||
"2026.3.0")
|
||||
std::string get_device_class() const;
|
||||
// Get unit of measurement as StringRef (from packed index)
|
||||
StringRef get_unit_of_measurement_ref() const;
|
||||
/// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref())
|
||||
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
|
||||
"removed in ESPHome 2026.9.0",
|
||||
"2026.3.0")
|
||||
std::string get_unit_of_measurement() const;
|
||||
|
||||
// Get/set this entity's icon
|
||||
ESPDEPRECATED(
|
||||
"Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0",
|
||||
"2025.11.0")
|
||||
std::string get_icon() const;
|
||||
void set_icon(const char *icon);
|
||||
StringRef get_icon_ref() const {
|
||||
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_);
|
||||
#else
|
||||
return EMPTY_STRING;
|
||||
#endif
|
||||
}
|
||||
StringRef get_icon_ref() const;
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
// Get/set this entity's device id
|
||||
@@ -173,9 +200,6 @@ class EntityBase {
|
||||
void calc_object_id_();
|
||||
|
||||
StringRef name_;
|
||||
#ifdef USE_ENTITY_ICON
|
||||
const char *icon_c_str_{nullptr};
|
||||
#endif
|
||||
uint32_t object_id_hash_{};
|
||||
#ifdef USE_DEVICES
|
||||
Device *device_{};
|
||||
@@ -190,44 +214,16 @@ class EntityBase {
|
||||
uint8_t entity_category : 2; // Supports up to 4 categories
|
||||
uint8_t reserved : 2; // Reserved for future use
|
||||
} flags_{};
|
||||
};
|
||||
|
||||
class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming)
|
||||
public:
|
||||
/// Get the device class, using the manual override if set.
|
||||
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in "
|
||||
"ESPHome 2026.5.0",
|
||||
"2025.11.0")
|
||||
std::string get_device_class();
|
||||
/// Manually set the device class.
|
||||
void set_device_class(const char *device_class);
|
||||
/// Get the device class as StringRef
|
||||
StringRef get_device_class_ref() const {
|
||||
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
|
||||
return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_);
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *device_class_{nullptr}; ///< Device class override
|
||||
};
|
||||
|
||||
class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
|
||||
public:
|
||||
/// Get the unit of measurement, using the manual override if set.
|
||||
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
|
||||
"removed in ESPHome 2026.5.0",
|
||||
"2025.11.0")
|
||||
std::string get_unit_of_measurement();
|
||||
/// Manually set the unit of measurement.
|
||||
void set_unit_of_measurement(const char *unit_of_measurement);
|
||||
/// Get the unit of measurement as StringRef
|
||||
StringRef get_unit_of_measurement_ref() const {
|
||||
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
|
||||
return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_);
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override
|
||||
// String table indices — packed into the 3 padding bytes after flags_
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
uint8_t device_class_idx_{};
|
||||
#endif
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
uint8_t uom_idx_{};
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
uint8_t icon_idx_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Log entity icon if set (for use in dump_config)
|
||||
@@ -240,10 +236,10 @@ inline void log_entity_icon(const char *, const char *, const EntityBase &) {}
|
||||
#endif
|
||||
/// Log entity device class if set (for use in dump_config)
|
||||
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj);
|
||||
/// Log entity unit of measurement if set (for use in dump_config)
|
||||
#define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj)
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj);
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj);
|
||||
|
||||
/**
|
||||
* An entity that has a state.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
import functools
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DISABLED_BY_DEFAULT,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -11,15 +14,184 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, add, get_variable
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.types import ConfigType, EntityMetadata
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "entity_string_pool"
|
||||
|
||||
# Private config keys for storing registered string indices
|
||||
_KEY_DC_IDX = "_entity_dc_idx"
|
||||
_KEY_UOM_IDX = "_entity_uom_idx"
|
||||
_KEY_ICON_IDX = "_entity_icon_idx"
|
||||
|
||||
# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h:
|
||||
# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits)
|
||||
_DC_SHIFT = 0
|
||||
_UOM_SHIFT = 8
|
||||
_ICON_SHIFT = 16
|
||||
|
||||
# Maximum unique strings per category (8-bit index, 0 = not set)
|
||||
_MAX_DEVICE_CLASSES = 0xFF # 255
|
||||
_MAX_UNITS = 0xFF # 255
|
||||
_MAX_ICONS = 0xFF # 255
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityStringPool:
|
||||
"""Pool of entity string properties for PROGMEM pointer tables.
|
||||
|
||||
Strings are registered during to_code() and assigned 1-based indices.
|
||||
Index 0 means "not set" (empty string). At render time, the pool
|
||||
generates C++ PROGMEM pointer table + lookup function per category.
|
||||
"""
|
||||
|
||||
device_classes: dict[str, int] = field(default_factory=dict)
|
||||
units: dict[str, int] = field(default_factory=dict)
|
||||
icons: dict[str, int] = field(default_factory=dict)
|
||||
tables_registered: bool = False
|
||||
|
||||
|
||||
def _get_pool() -> EntityStringPool:
|
||||
"""Get or create the entity string pool from CORE.data."""
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = EntityStringPool()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def _ensure_tables_registered() -> None:
|
||||
"""Schedule the table generation job (once)."""
|
||||
pool = _get_pool()
|
||||
if pool.tables_registered:
|
||||
return
|
||||
pool.tables_registered = True
|
||||
CORE.add_job(_generate_tables_job)
|
||||
|
||||
|
||||
def _generate_category_code(
|
||||
table_var: str,
|
||||
lookup_fn: str,
|
||||
strings: dict[str, int],
|
||||
) -> str:
|
||||
"""Generate C++ code for one string category (PROGMEM pointer table + lookup).
|
||||
|
||||
Uses a PROGMEM array of string pointers. On ESP8266, pointers are stored
|
||||
in flash (via PROGMEM) and read with progmem_read_ptr(). String literals
|
||||
themselves remain in RAM but benefit from linker string deduplication.
|
||||
Index 0 means "not set" and returns empty string.
|
||||
"""
|
||||
if not strings:
|
||||
return ""
|
||||
|
||||
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
|
||||
entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings)
|
||||
count = len(sorted_strings)
|
||||
|
||||
return (
|
||||
f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n"
|
||||
f"const char *{lookup_fn}(uint8_t index) {{\n"
|
||||
f' if (index == 0 || index > {count}) return "";\n'
|
||||
f" return progmem_read_ptr(&{table_var}[index - 1]);\n"
|
||||
f"}}\n"
|
||||
)
|
||||
|
||||
|
||||
_CATEGORY_CONFIGS = (
|
||||
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"),
|
||||
("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"),
|
||||
("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _generate_tables_job() -> None:
|
||||
"""Generate all entity string table C++ code as a FINAL-priority job.
|
||||
|
||||
Runs after all component to_code() calls have registered their strings.
|
||||
"""
|
||||
pool = _get_pool()
|
||||
parts = ["namespace esphome {"]
|
||||
for table_var, lookup_fn, attr in _CATEGORY_CONFIGS:
|
||||
code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr))
|
||||
if code:
|
||||
parts.append(code)
|
||||
parts.append("} // namespace esphome")
|
||||
cg.add_global(RawStatement("\n".join(parts)))
|
||||
|
||||
|
||||
def _register_string(
|
||||
value: str, category: dict[str, int], max_count: int, category_name: str
|
||||
) -> int:
|
||||
"""Register a string in a category dict and return its 1-based index.
|
||||
|
||||
Returns 0 if value is empty/None (meaning "not set").
|
||||
"""
|
||||
if not value:
|
||||
return 0
|
||||
if value in category:
|
||||
return category[value]
|
||||
idx = len(category) + 1
|
||||
if idx > max_count:
|
||||
raise ValueError(
|
||||
f"Too many unique {category_name} values (max {max_count}), got {idx}: '{value}'"
|
||||
)
|
||||
category[value] = idx
|
||||
_ensure_tables_registered()
|
||||
return idx
|
||||
|
||||
|
||||
def register_device_class(value: str) -> int:
|
||||
"""Register a device_class string and return its 1-based index."""
|
||||
return _register_string(
|
||||
value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class"
|
||||
)
|
||||
|
||||
|
||||
def register_unit_of_measurement(value: str) -> int:
|
||||
"""Register a unit_of_measurement string and return its 1-based index."""
|
||||
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
|
||||
|
||||
|
||||
def register_icon(value: str) -> int:
|
||||
"""Register an icon string and return its 1-based index."""
|
||||
return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon")
|
||||
|
||||
|
||||
def setup_device_class(config: ConfigType) -> None:
|
||||
"""Register config's device_class and store its index for finalize_entity_strings."""
|
||||
idx = register_device_class(config.get(CONF_DEVICE_CLASS, ""))
|
||||
if idx:
|
||||
cg.add_define("USE_ENTITY_DEVICE_CLASS")
|
||||
config[_KEY_DC_IDX] = idx
|
||||
|
||||
|
||||
def setup_unit_of_measurement(config: ConfigType) -> None:
|
||||
"""Register config's unit_of_measurement and store its index for finalize_entity_strings."""
|
||||
idx = register_unit_of_measurement(config.get(CONF_UNIT_OF_MEASUREMENT, ""))
|
||||
if idx:
|
||||
cg.add_define("USE_ENTITY_UNIT_OF_MEASUREMENT")
|
||||
config[_KEY_UOM_IDX] = idx
|
||||
|
||||
|
||||
def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
|
||||
"""Emit a single set_entity_strings() call with all packed indices.
|
||||
|
||||
Call this at the end of each component's setup function, after
|
||||
setup_entity() and any register_device_class/register_unit_of_measurement calls.
|
||||
"""
|
||||
dc_idx = config.get(_KEY_DC_IDX, 0)
|
||||
uom_idx = config.get(_KEY_UOM_IDX, 0)
|
||||
icon_idx = config.get(_KEY_ICON_IDX, 0)
|
||||
packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT)
|
||||
if packed != 0:
|
||||
add(var.set_entity_strings(packed))
|
||||
|
||||
|
||||
def get_base_entity_object_id(
|
||||
name: str, friendly_name: str | None, device_name: str | None = None
|
||||
@@ -64,8 +236,48 @@ def get_base_entity_object_id(
|
||||
return sanitize(snake_case(base_str))
|
||||
|
||||
|
||||
async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"""Set up generic properties of an Entity.
|
||||
def setup_entity(var_or_platform, config=None, platform=None):
|
||||
"""Set up entity properties — works as both decorator and direct call.
|
||||
|
||||
Decorator mode::
|
||||
|
||||
@setup_entity("sensor")
|
||||
async def setup_sensor_core_(var, config):
|
||||
setup_device_class(config)
|
||||
setup_unit_of_measurement(config)
|
||||
...
|
||||
|
||||
Direct call mode (for entities with no extra string properties)::
|
||||
|
||||
await setup_entity(var, config, "camera")
|
||||
"""
|
||||
if isinstance(var_or_platform, str) and config is None:
|
||||
# Decorator mode: @setup_entity("sensor")
|
||||
platform = var_or_platform
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
async def wrapper(
|
||||
var: MockObj, config: ConfigType, *args, **kwargs
|
||||
) -> None:
|
||||
await _setup_entity_impl(var, config, platform)
|
||||
await func(var, config, *args, **kwargs)
|
||||
finalize_entity_strings(var, config)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
# Direct call mode: await setup_entity(var, config, "camera")
|
||||
async def _do() -> None:
|
||||
await _setup_entity_impl(var_or_platform, config, platform)
|
||||
finalize_entity_strings(var_or_platform, config)
|
||||
|
||||
return _do()
|
||||
|
||||
|
||||
async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"""Set up generic properties of an Entity (internal implementation).
|
||||
|
||||
This function sets up the common entity properties like name, icon,
|
||||
entity category, etc.
|
||||
@@ -92,12 +304,15 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
add(var.set_disabled_by_default(True))
|
||||
if CONF_INTERNAL in config:
|
||||
add(var.set_internal(config[CONF_INTERNAL]))
|
||||
icon_idx = 0
|
||||
if CONF_ICON in config:
|
||||
# Add USE_ENTITY_ICON define when icons are used
|
||||
cg.add_define("USE_ENTITY_ICON")
|
||||
add(var.set_icon(config[CONF_ICON]))
|
||||
icon_idx = register_icon(config[CONF_ICON])
|
||||
if CONF_ENTITY_CATEGORY in config:
|
||||
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
|
||||
# Store icon index for finalize_entity_strings
|
||||
config[_KEY_ICON_IDX] = icon_idx
|
||||
|
||||
|
||||
def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
|
||||
|
||||
@@ -42,6 +42,7 @@ void arch_feed_wdt();
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
uint8_t progmem_read_byte(const uint8_t *addr);
|
||||
const char *progmem_read_ptr(const char *const *addr);
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr);
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -79,6 +79,7 @@ def clang_options(idedata):
|
||||
"-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))",
|
||||
"-Dpgm_read_word(s)=(*(const uint16_t *)(s))",
|
||||
"-Dpgm_read_dword(s)=(*(const uint32_t *)(s))",
|
||||
"-Dpgm_read_ptr(s)=(*(const void *const *)(s))",
|
||||
"-DPROGMEM=",
|
||||
"-DPGM_P=const char *",
|
||||
"-DPSTR(s)=(s)",
|
||||
|
||||
@@ -11,4 +11,4 @@ def test_sensor_device_class_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 's_1->set_device_class("voltage");' in main_cpp
|
||||
assert "s_1->set_entity_strings(" in main_cpp
|
||||
|
||||
@@ -54,5 +54,5 @@ def test_text_sensor_device_class_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_2->set_device_class("timestamp");' in main_cpp
|
||||
assert 'ts_3->set_device_class("date");' in main_cpp
|
||||
assert "ts_2->set_entity_strings(" in main_cpp
|
||||
assert "ts_3->set_entity_strings(" in main_cpp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user