mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 13:09:12 +00:00
373 lines
17 KiB
C++
373 lines
17 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <span>
|
|
#include <string>
|
|
#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 { return this->name_; }
|
|
|
|
// 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 unique Object ID of this Entity
|
|
uint32_t get_object_id_hash() const { return this->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<char, OBJECT_ID_MAX_LEN> 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; }
|
|
|
|
// Deprecated: Calling set_internal() at runtime is undefined behavior. Components and clients
|
|
// are NOT notified of the change, the flag may have already been read during setup, and there
|
|
// is NO guarantee any consumer will observe the new value. Use the 'internal:' YAML key instead.
|
|
ESPDEPRECATED("set_internal() is undefined behavior at runtime — components and Home Assistant are NOT "
|
|
"notified. Use the 'internal:' YAML key instead. Will be removed in 2027.3.0.",
|
|
"2026.3.0")
|
|
void set_internal(bool internal) { this->flags_.internal = 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<EntityCategory>(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<char, MAX_DEVICE_CLASS_LENGTH> 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<typename T = int> 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<typename T = int> 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<char, MAX_ICON_LENGTH> 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<typename T = int> 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<typename T = int> 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<T>() instead, or preferences won't be migrated.
|
|
* See https://github.com/esphome/backlog/issues/85
|
|
*/
|
|
ESPDEPRECATED("Use make_entity_preference<T>() 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<typename T> ESPPreferenceObject make_entity_preference(uint32_t version = 0) {
|
|
static_assert(std::is_trivially_copyable<T>::value, "T must be trivially copyable");
|
|
return this->make_entity_preference_(sizeof(T), version);
|
|
}
|
|
|
|
protected:
|
|
friend void ::setup();
|
|
friend void ::original_setup();
|
|
// Application's register_<entity>(obj, name, hash, fields) overloads call configure_entity_
|
|
// before push_back, so codegen can emit a single combined call per entity.
|
|
friend class Application;
|
|
|
|
/// 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);
|
|
|
|
/** Base class for entities that track a typed state value with change-detection and callbacks.
|
|
*
|
|
* This class does not store the state value — subclasses own their storage. Whether a state
|
|
* has been set is tracked by EntityBase::has_state().
|
|
*
|
|
* Subclasses must implement:
|
|
* - get_state(): return a const reference to the current value
|
|
* - set_state_value(): store a new value (called only when the state actually changes)
|
|
* - get_trigger_on_initial_state(): return whether callbacks should fire on the first state
|
|
*
|
|
* Subclasses may override set_new_state() to add behavior (logging, notifications) after calling
|
|
* the base implementation. Since set_new_state() is virtual, callers like invalidate_state()
|
|
* dispatch through the vtable to the subclass override in the .cpp, avoiding template code
|
|
* bloat at inline call sites. Subclasses may also add a fast-path dedup check before calling
|
|
* set_new_state() to skip virtual dispatch entirely when the state hasn't changed.
|
|
*
|
|
* Callback behavior:
|
|
* - full_state_callbacks_: fired on every change, receives optional<T> previous and current
|
|
* - state_callbacks_: fired only when the new state has a value, and either this is not the
|
|
* first state (had_state) or trigger_on_initial_state is set
|
|
*
|
|
* @tparam T The type of the state value
|
|
*/
|
|
template<typename T> class StatefulEntityBase : public EntityBase {
|
|
public:
|
|
/// Return the current state value. Only valid when has_state() is true.
|
|
virtual const T &get_state() const = 0;
|
|
/// Return the current state if available, otherwise return the provided default.
|
|
T get_state_default(T default_value) const { return this->has_state() ? this->get_state() : default_value; }
|
|
/// Clear the state — sets has_state() to false and fires callbacks with nullopt.
|
|
void invalidate_state() { this->set_new_state({}); }
|
|
|
|
template<typename F> void add_full_state_callback(F &&callback) {
|
|
this->full_state_callbacks_.add(std::forward<F>(callback));
|
|
}
|
|
template<typename F> void add_on_state_callback(F &&callback) {
|
|
this->state_callbacks_.add(std::forward<F>(callback));
|
|
}
|
|
|
|
protected:
|
|
/// Subclasses return whether callbacks should fire on the very first state.
|
|
virtual bool get_trigger_on_initial_state() const = 0;
|
|
|
|
/** Apply a new state, de-duplicating and firing callbacks as needed.
|
|
*
|
|
* Pass nullopt to invalidate (clear) the state. Pass a value to set it.
|
|
* Returns true if the state actually changed, false if it was the same.
|
|
* Subclasses may override to add logging/notifications after calling the base.
|
|
*/
|
|
virtual bool set_new_state(const optional<T> &new_state) {
|
|
// Access flags_ directly to avoid function call overhead in this hot path
|
|
bool had_state = this->flags_.has_state;
|
|
// Use pointer to avoid requiring T to be default-constructible
|
|
const T *current = had_state ? &this->get_state() : nullptr;
|
|
if (new_state.has_value()) {
|
|
if (current != nullptr && *current == new_state.value())
|
|
return false; // same value, no change
|
|
} else if (!had_state) {
|
|
return false; // already invalidated, no change
|
|
}
|
|
// Capture old_state before set_state_value — current pointer aliases subclass storage
|
|
bool has_full_cbs = !this->full_state_callbacks_.empty();
|
|
optional<T> old_state;
|
|
if (has_full_cbs)
|
|
old_state = current != nullptr ? optional<T>(*current) : nullopt;
|
|
// Update storage before firing callbacks so callback code can inspect current state
|
|
this->flags_.has_state = new_state.has_value();
|
|
if (new_state.has_value()) {
|
|
this->set_state_value(new_state.value());
|
|
}
|
|
if (has_full_cbs)
|
|
this->full_state_callbacks_.call(old_state, new_state);
|
|
// had_state first: on every change except the first, skips the virtual call
|
|
if (new_state.has_value() && (had_state || this->get_trigger_on_initial_state()))
|
|
this->state_callbacks_.call(new_state.value());
|
|
return true;
|
|
}
|
|
/// Subclasses implement this to store the actual value into their own storage.
|
|
virtual void set_state_value(const T &value) = 0;
|
|
LazyCallbackManager<void(optional<T> previous, optional<T> current)> full_state_callbacks_;
|
|
LazyCallbackManager<void(T)> state_callbacks_;
|
|
};
|
|
} // namespace esphome
|