#pragma once #include #include #include #include "string_ref.h" #include "helpers.h" #include "log.h" #include "preferences.h" #ifdef USE_DEVICES #include "device.h" #endif // Forward declarations for friend access from codegen-generated setup() void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests 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; // Maximum friendly name length for entities and sub-devices - keep in sync with FRIENDLY_NAME_MAX_LEN in // esphome/core/config.py static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120; // Maximum domain length (longest: "alarm_control_panel" = 19) static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20; // Maximum size for object_id buffer (friendly_name + null + margin) static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; // Maximum device class string buffer size (47 chars + null terminator) // Longest standard device class: "volatile_organic_compounds_parts" (32 chars) // Device classes are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. static constexpr size_t MAX_DEVICE_CLASS_LENGTH = 48; // Maximum icon string buffer size (63 chars + null terminator) // Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. static constexpr size_t MAX_ICON_LENGTH = 64; enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, ENTITY_CATEGORY_DIAGNOSTIC = 2, }; // Bit layout for entity_fields parameter in configure_entity_(). // Keep in sync with _*_SHIFT constants in esphome/core/entity_helpers.py static constexpr uint8_t ENTITY_FIELD_DC_SHIFT = 0; static constexpr uint8_t ENTITY_FIELD_UOM_SHIFT = 8; static constexpr uint8_t ENTITY_FIELD_ICON_SHIFT = 16; static constexpr uint8_t ENTITY_FIELD_INTERNAL_SHIFT = 24; static constexpr uint8_t ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT = 25; static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26; // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: // Get the name of this Entity const StringRef &get_name() const; // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } // Get the sanitized name of this Entity as an ID. // Deprecated: object_id mangles names and all object_id methods are planned for removal. // See https://github.com/esphome/backlog/issues/76 // Now is the time to stop using object_id entirely. If you still need it temporarily, // use get_object_id_to() which will remain available longer but will also eventually be removed. ESPDEPRECATED("object_id mangles names and all object_id methods are planned for removal " "(see https://github.com/esphome/backlog/issues/76). " "Now is the time to stop using object_id. If still needed, use get_object_id_to() " "which will remain available longer. get_object_id() will be removed in 2026.7.0", "2025.12.0") std::string get_object_id() const; // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); /// Get object_id with zero heap allocation /// For static case: returns StringRef to internal storage (buffer unused) /// For dynamic case: formats into buffer and returns StringRef to buffer StringRef get_object_id_to(std::span buf) const; /// Write object_id directly to buffer, returns length written (excluding null) /// Useful for building compound strings without intermediate buffer size_t write_object_id_to(char *buf, size_t buf_size) const; // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } // Get the entity category. EntityCategory get_entity_category() const { return static_cast(this->flags_.entity_category); } // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. const char *get_device_class_to(std::span buffer) const; #ifdef USE_ESP8266 // On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed // directly as const char*. Use get_device_class_to() with a stack buffer instead. template StringRef get_device_class_ref() const { static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). " "Use get_device_class_to() with a stack buffer."); return StringRef(""); } template std::string get_device_class() const { static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). " "Use get_device_class_to() with a stack buffer."); return ""; } #else // Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM. ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_device_class_ref() const; ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") std::string get_device_class() const; #endif // 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 this entity's icon into a stack buffer. // On ESP32: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. const char *get_icon_to(std::span buffer) const; #ifdef USE_ESP8266 // On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed // directly as const char*. Use get_icon_to() with a stack buffer instead. template StringRef get_icon_ref() const { static_assert(sizeof(T) == 0, "get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); return StringRef(""); } template std::string get_icon() const { static_assert(sizeof(T) == 0, "get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); return ""; } #else // Deprecated: use get_icon_to() instead. Icons are in PROGMEM. ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_icon_ref() const; ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") std::string get_icon() const; #endif #ifdef USE_DEVICES // Get this entity's device id uint32_t get_device_id() const { if (this->device_ == nullptr) { return 0; // No device set, return 0 } return this->device_->get_device_id(); } // Get the device this entity belongs to (nullptr if main device) Device *get_device() const { return this->device_; } #endif // Check if this entity has state bool has_state() const { return this->flags_.has_state; } // Set has_state - for components that need to manually set this void set_has_state(bool state) { this->flags_.has_state = state; } /** * @brief Get a unique hash for storing preferences/settings for this entity. * * This method returns a hash that uniquely identifies the entity for the purpose of * storing preferences (such as calibration, state, etc.). Unlike get_object_id_hash(), * this hash also incorporates the device_id (if devices are enabled), ensuring uniqueness * across multiple devices that may have entities with the same object_id. * * Use this method when storing or retrieving preferences/settings that should be unique * per device-entity pair. Use get_object_id_hash() when you need a hash that identifies * the entity regardless of the device it belongs to. * * For backward compatibility, if device_id is 0 (the main device), the hash is unchanged * from previous versions, so existing single-device configurations will continue to work. * * @return uint32_t The unique hash for preferences, including device_id if available. * @deprecated Use make_entity_preference() instead, or preferences won't be migrated. * See https://github.com/esphome/backlog/issues/85 */ ESPDEPRECATED("Use make_entity_preference() instead, or preferences won't be migrated. " "See https://github.com/esphome/backlog/issues/85. Will be removed in 2027.1.0.", "2026.7.0") uint32_t get_preference_hash() { #ifdef USE_DEVICES // Combine object_id_hash with device_id to ensure uniqueness across devices // Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash // This ensures backward compatibility for existing single-device configurations return this->get_object_id_hash() ^ this->get_device_id(); #else // Without devices, just use object_id_hash as before return this->get_object_id_hash(); #endif } /// Create a preference object for storing this entity's state/settings. /// @tparam T The type of data to store (must be trivially copyable) /// @param version Optional version hash XORed with preference key (change when struct layout changes) template ESPPreferenceObject make_entity_preference(uint32_t version = 0) { static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); return this->make_entity_preference_(sizeof(T), version); } protected: friend void ::setup(); friend void ::original_setup(); /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags. /// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above. void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields); #ifdef USE_DEVICES // Codegen-only setter — only accessible from setup() via friend declaration. void set_device_(Device *device) { this->device_ = device; } #endif /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); void calc_object_id_(); StringRef name_; uint32_t object_id_hash_{}; #ifdef USE_DEVICES Device *device_{}; #endif // Bit-packed flags to save memory (1 byte instead of 5) struct EntityFlags { uint8_t has_own_name : 1; uint8_t internal : 1; uint8_t disabled_by_default : 1; uint8_t has_state : 1; uint8_t entity_category : 2; // Supports up to 4 categories uint8_t reserved : 2; // Reserved for future use } flags_{}; // 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) #ifdef USE_ENTITY_ICON #define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj) void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj); #else #define LOG_ENTITY_ICON(tag, prefix, obj) ((void) 0) 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 &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 &obj); /** * An entity that has a state. * @tparam T The type of the state */ template class StatefulEntityBase : public EntityBase { public: virtual bool has_state() const { return this->state_.has_value(); } virtual const T &get_state() const { return this->state_.value(); } // NOLINT(bugprone-unchecked-optional-access) virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } void invalidate_state() { this->set_new_state({}); } void add_full_state_callback(std::function previous, optional current)> &&callback) { if (this->full_state_callbacks_ == nullptr) this->full_state_callbacks_ = new CallbackManager previous, optional current)>(); // NOLINT this->full_state_callbacks_->add(std::move(callback)); } void add_on_state_callback(std::function &&callback) { if (this->state_callbacks_ == nullptr) this->state_callbacks_ = new CallbackManager(); // NOLINT this->state_callbacks_->add(std::move(callback)); } void set_trigger_on_initial_state(bool trigger_on_initial_state) { this->trigger_on_initial_state_ = trigger_on_initial_state; } protected: optional state_{}; /** * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous. * * @param new_state The new state. * @return True if the state was changed, false if it was the same as before. */ virtual bool set_new_state(const optional &new_state) { if (this->state_ != new_state) { // call the full state callbacks with the previous and new state if (this->full_state_callbacks_ != nullptr) this->full_state_callbacks_->call(this->state_, new_state); // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or // the previous state was valid auto had_state = this->has_state(); this->state_ = new_state; if (this->state_callbacks_ != nullptr && new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) this->state_callbacks_->call(new_state.value()); return true; } return false; } bool trigger_on_initial_state_{true}; // callbacks with full state and previous state CallbackManager previous, optional current)> *full_state_callbacks_{}; CallbackManager *state_callbacks_{}; }; } // namespace esphome