mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 18:48:39 +00:00
340 lines
16 KiB
C++
340 lines
16 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; }
|
|
|
|
// Set whether this Entity should be hidden outside ESPHome. Prefer the 'internal:' YAML key
|
|
// whenever possible: it is guaranteed and has none of the limitations below. Use this only when
|
|
// the decision can only be made at boot. Must be called before MQTT and the API read the flag:
|
|
// from on_boot at the default priority, or a setup() that runs above setup_priority::AFTER_WIFI.
|
|
// If the answer comes from a device handshake, hold setup with can_proceed() until it arrives.
|
|
// Calls after setup finishes are undefined behavior: the flag is still written and an error is
|
|
// logged, and from 2027.3.0 the call will be ignored.
|
|
//
|
|
// Known limitations. Not bugs, so no issue reports please; a PR that removes one with no RAM
|
|
// or performance cost would be considered.
|
|
// - No consumer is notified of a change, so the flag can only be decided once per boot.
|
|
// - The guard is coarse: a call from a priority below AFTER_WIFI (an on_boot with a low priority,
|
|
// or a setup() at LATE) still passes, but the API camera listener is already registered, MQTT
|
|
// (AFTER_CONNECTION) has cached the flag, and an API client that connected while setup was
|
|
// stalled on a slow component has already listed the entities, so they keep the old value.
|
|
// - Un-hiding an entity declared 'internal: true' in YAML skips the duplicate name check that
|
|
// codegen runs for exposed entities, so a name collision can surface at runtime. Entities with
|
|
// only an 'id:' are forced internal and use the id as their name.
|
|
// - Zigbee codegen skips YAML internal entities entirely, so un-hiding cannot add them to Zigbee.
|
|
void set_internal(bool 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;
|
|
|
|
// Get unit of measurement as StringRef (from packed index)
|
|
StringRef get_unit_of_measurement_ref() 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_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 the 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
|