mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/has_state_' into integration
This commit is contained in:
		| @@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity"; | ||||
|  | ||||
| void DateEntity::publish_state() { | ||||
|   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     return; | ||||
|   } | ||||
|   if (this->year_ < 1970 || this->year_ > 3000) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Year must be between 1970 and 3000"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->month_ < 1 || this->month_ > 12) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Month must be between 1 and 12"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->day_ > days_in_month(this->month_, this->year_)) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); | ||||
|     return; | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); | ||||
|   this->state_callback_.call(); | ||||
| } | ||||
|   | ||||
| @@ -13,9 +13,6 @@ namespace datetime { | ||||
|  | ||||
| class DateTimeBase : public EntityBase { | ||||
|  public: | ||||
|   /// Return whether this Datetime has gotten a full state yet. | ||||
|   bool has_state() const { return this->has_state_; } | ||||
|  | ||||
|   virtual ESPTime state_as_esptime() const = 0; | ||||
|  | ||||
|   void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } | ||||
| @@ -31,8 +28,6 @@ class DateTimeBase : public EntityBase { | ||||
| #ifdef USE_TIME | ||||
|   time::RealTimeClock *rtc_; | ||||
| #endif | ||||
|  | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   | ||||
| @@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity"; | ||||
|  | ||||
| void DateTimeEntity::publish_state() { | ||||
|   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     return; | ||||
|   } | ||||
|   if (this->year_ < 1970 || this->year_ > 3000) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Year must be between 1970 and 3000"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->month_ < 1 || this->month_ > 12) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Month must be between 1 and 12"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->day_ > days_in_month(this->month_, this->year_)) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); | ||||
|     return; | ||||
|   } | ||||
|   if (this->hour_ > 23) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Hour must be between 0 and 23"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->minute_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Minute must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->second_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Second must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, | ||||
|            this->month_, this->day_, this->hour_, this->minute_, this->second_); | ||||
|   this->state_callback_.call(); | ||||
|   | ||||
| @@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity"; | ||||
|  | ||||
| void TimeEntity::publish_state() { | ||||
|   if (this->hour_ > 23) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Hour must be between 0 and 23"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->minute_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Minute must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->second_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Second must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, | ||||
|            this->second_); | ||||
|   this->state_callback_.call(); | ||||
|   | ||||
| @@ -57,7 +57,7 @@ void ESP32Camera::dump_config() { | ||||
|                 "  External Clock: Pin:%d Frequency:%u\n" | ||||
|                 "  I2C Pins: SDA:%d SCL:%d\n" | ||||
|                 "  Reset Pin: %d", | ||||
|                 this->name_.c_str(), YESNO(this->internal_), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, | ||||
|                 this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, | ||||
|                 conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, | ||||
|                 conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); | ||||
|   switch (this->config_.frame_size) { | ||||
|   | ||||
| @@ -56,7 +56,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti | ||||
|     this->publish_state(state); | ||||
|   } else { | ||||
|     this->state = state; | ||||
|     this->has_state_ = true; | ||||
|     this->set_has_state(true); | ||||
|   } | ||||
|  | ||||
|   this->update_component_settings(); | ||||
|   | ||||
| @@ -88,7 +88,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { | ||||
|     } else { | ||||
|       this->raw_state = state; | ||||
|       this->state = state; | ||||
|       this->has_state_ = true; | ||||
|       this->set_has_state(true); | ||||
|     } | ||||
|   } | ||||
|   this->update_component_settings(); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s | ||||
|     this->publish_state(state); | ||||
|   } else { | ||||
|     this->state = state; | ||||
|     this->has_state_ = true; | ||||
|     this->set_has_state(true); | ||||
|   } | ||||
|  | ||||
|   this->update_component_settings(); | ||||
|   | ||||
| @@ -7,7 +7,7 @@ namespace number { | ||||
| static const char *const TAG = "number"; | ||||
|  | ||||
| void Number::publish_state(float state) { | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   this->state = state; | ||||
|   ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); | ||||
|   this->state_callback_.call(state); | ||||
|   | ||||
| @@ -48,9 +48,6 @@ class Number : public EntityBase { | ||||
|  | ||||
|   NumberTraits traits; | ||||
|  | ||||
|   /// Return whether this number has gotten a full state yet. | ||||
|   bool has_state() const { return has_state_; } | ||||
|  | ||||
|  protected: | ||||
|   friend class NumberCall; | ||||
|  | ||||
| @@ -63,7 +60,6 @@ class Number : public EntityBase { | ||||
|   virtual void control(float value) = 0; | ||||
|  | ||||
|   CallbackManager<void(float)> state_callback_; | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace number | ||||
|   | ||||
| @@ -10,7 +10,7 @@ void Select::publish_state(const std::string &state) { | ||||
|   auto index = this->index_of(state); | ||||
|   const auto *name = this->get_name().c_str(); | ||||
|   if (index.has_value()) { | ||||
|     this->has_state_ = true; | ||||
|     this->set_has_state(true); | ||||
|     this->state = state; | ||||
|     ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); | ||||
|     this->state_callback_.call(state, index.value()); | ||||
|   | ||||
| @@ -35,9 +35,6 @@ class Select : public EntityBase { | ||||
|  | ||||
|   void publish_state(const std::string &state); | ||||
|  | ||||
|   /// Return whether this select component has gotten a full state yet. | ||||
|   bool has_state() const { return has_state_; } | ||||
|  | ||||
|   /// Instantiate a SelectCall object to modify this select component's state. | ||||
|   SelectCall make_call() { return SelectCall(this); } | ||||
|  | ||||
| @@ -73,7 +70,6 @@ class Select : public EntityBase { | ||||
|   virtual void control(const std::string &value) = 0; | ||||
|  | ||||
|   CallbackManager<void(std::string, size_t)> state_callback_; | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace select | ||||
|   | ||||
| @@ -93,13 +93,12 @@ float Sensor::get_raw_state() const { return this->raw_state; } | ||||
| std::string Sensor::unique_id() { return ""; } | ||||
|  | ||||
| void Sensor::internal_send_state_to_frontend(float state) { | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   this->state = state; | ||||
|   ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, | ||||
|            this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); | ||||
|   this->callback_.call(state); | ||||
| } | ||||
| bool Sensor::has_state() const { return this->has_state_; } | ||||
|  | ||||
| }  // namespace sensor | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -141,9 +141,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | ||||
|    */ | ||||
|   float raw_state; | ||||
|  | ||||
|   /// Return whether this sensor has gotten a full state (that passed through all filters) yet. | ||||
|   bool has_state() const; | ||||
|  | ||||
|   /** Override this method to set the unique ID of this sensor. | ||||
|    * | ||||
|    * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). | ||||
| @@ -161,7 +158,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | ||||
|   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override | ||||
|   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override | ||||
|   bool force_update_{false};                            ///< Force update mode | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace sensor | ||||
|   | ||||
| @@ -7,7 +7,7 @@ namespace text { | ||||
| static const char *const TAG = "text"; | ||||
|  | ||||
| void Text::publish_state(const std::string &state) { | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   this->state = state; | ||||
|   if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { | ||||
|     ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str()); | ||||
|   | ||||
| @@ -28,9 +28,6 @@ class Text : public EntityBase { | ||||
|  | ||||
|   void publish_state(const std::string &state); | ||||
|  | ||||
|   /// Return whether this text input has gotten a full state yet. | ||||
|   bool has_state() const { return has_state_; } | ||||
|  | ||||
|   /// Instantiate a TextCall object to modify this text component's state. | ||||
|   TextCall make_call() { return TextCall(this); } | ||||
|  | ||||
| @@ -48,7 +45,6 @@ class Text : public EntityBase { | ||||
|   virtual void control(const std::string &value) = 0; | ||||
|  | ||||
|   CallbackManager<void(std::string)> state_callback_; | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace text | ||||
|   | ||||
| @@ -65,13 +65,12 @@ std::string TextSensor::get_state() const { return this->state; } | ||||
| std::string TextSensor::get_raw_state() const { return this->raw_state; } | ||||
| void TextSensor::internal_send_state_to_frontend(const std::string &state) { | ||||
|   this->state = state; | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); | ||||
|   this->callback_.call(state); | ||||
| } | ||||
|  | ||||
| std::string TextSensor::unique_id() { return ""; } | ||||
| bool TextSensor::has_state() { return this->has_state_; } | ||||
|  | ||||
| }  // namespace text_sensor | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -70,8 +70,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { | ||||
|    */ | ||||
|   virtual std::string unique_id(); | ||||
|  | ||||
|   bool has_state(); | ||||
|  | ||||
|   void internal_send_state_to_frontend(const std::string &state); | ||||
|  | ||||
|  protected: | ||||
| @@ -80,8 +78,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { | ||||
|   CallbackManager<void(std::string)> callback_;  ///< Storage for filtered state callbacks. | ||||
|  | ||||
|   Filter *filter_list_{nullptr};  ///< Store all active filters. | ||||
|  | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace text_sensor | ||||
|   | ||||
| @@ -30,7 +30,7 @@ void UpdateEntity::publish_state() { | ||||
|     ESP_LOGD(TAG, "  Progress: %.0f%%", this->update_info_.progress); | ||||
|   } | ||||
|  | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   this->state_callback_.call(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -28,8 +28,6 @@ enum UpdateState : uint8_t { | ||||
|  | ||||
| class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { | ||||
|  public: | ||||
|   bool has_state() const { return this->has_state_; } | ||||
|  | ||||
|   void publish_state(); | ||||
|  | ||||
|   void perform() { this->perform(false); } | ||||
| @@ -44,7 +42,6 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { | ||||
|  protected: | ||||
|   UpdateState state_{UPDATE_STATE_UNKNOWN}; | ||||
|   UpdateInfo update_info_; | ||||
|   bool has_state_{false}; | ||||
|  | ||||
|   CallbackManager<void()> state_callback_{}; | ||||
| }; | ||||
|   | ||||
| @@ -13,7 +13,7 @@ static const char *const TAG = "uptime.sensor"; | ||||
|  | ||||
| void UptimeTimestampSensor::setup() { | ||||
|   this->time_->add_on_time_sync_callback([this]() { | ||||
|     if (this->has_state_) | ||||
|     if (this->has_state()) | ||||
|       return;  // No need to update the timestamp if it's already set | ||||
|  | ||||
|     auto now = this->time_->now(); | ||||
|   | ||||
| @@ -12,20 +12,12 @@ void EntityBase::set_name(const char *name) { | ||||
|   this->name_ = StringRef(name); | ||||
|   if (this->name_.empty()) { | ||||
|     this->name_ = StringRef(App.get_friendly_name()); | ||||
|     this->has_own_name_ = false; | ||||
|     this->flags_.has_own_name = false; | ||||
|   } else { | ||||
|     this->has_own_name_ = true; | ||||
|     this->flags_.has_own_name = true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Entity Internal | ||||
| bool EntityBase::is_internal() const { return this->internal_; } | ||||
| void EntityBase::set_internal(bool internal) { this->internal_ = internal; } | ||||
|  | ||||
| // Entity Disabled by Default | ||||
| bool EntityBase::is_disabled_by_default() const { return this->disabled_by_default_; } | ||||
| void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; } | ||||
|  | ||||
| // Entity Icon | ||||
| std::string EntityBase::get_icon() const { | ||||
|   if (this->icon_c_str_ == nullptr) { | ||||
| @@ -35,14 +27,10 @@ std::string EntityBase::get_icon() const { | ||||
| } | ||||
| void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; } | ||||
|  | ||||
| // Entity Category | ||||
| EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; } | ||||
| void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; } | ||||
|  | ||||
| // Entity Object ID | ||||
| std::string EntityBase::get_object_id() const { | ||||
|   // Check if `App.get_friendly_name()` is constant or dynamic. | ||||
|   if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { | ||||
|   if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { | ||||
|     // `App.get_friendly_name()` is dynamic. | ||||
|     return str_sanitize(str_snake_case(App.get_friendly_name())); | ||||
|   } else { | ||||
| @@ -61,7 +49,7 @@ void EntityBase::set_object_id(const char *object_id) { | ||||
| // Calculate Object ID Hash from Entity Name | ||||
| void EntityBase::calc_object_id_() { | ||||
|   // Check if `App.get_friendly_name()` is constant or dynamic. | ||||
|   if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { | ||||
|   if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { | ||||
|     // `App.get_friendly_name()` is dynamic. | ||||
|     const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); | ||||
|     // FNV-1 hash | ||||
|   | ||||
| @@ -22,7 +22,7 @@ class EntityBase { | ||||
|   void set_name(const char *name); | ||||
|  | ||||
|   // Get whether this Entity has its own name or it should use the device friendly_name. | ||||
|   bool has_own_name() const { return this->has_own_name_; } | ||||
|   bool has_own_name() const { return this->flags_.has_own_name; } | ||||
|  | ||||
|   // Get the sanitized name of this Entity as an ID. | ||||
|   std::string get_object_id() const; | ||||
| @@ -32,23 +32,31 @@ class EntityBase { | ||||
|   uint32_t get_object_id_hash(); | ||||
|  | ||||
|   // Get/set whether this Entity should be hidden outside ESPHome | ||||
|   bool is_internal() const; | ||||
|   void set_internal(bool internal); | ||||
|   bool is_internal() const { return this->flags_.internal; } | ||||
|   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; | ||||
|   void set_disabled_by_default(bool disabled_by_default); | ||||
|   bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } | ||||
|   void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; } | ||||
|  | ||||
|   // Get/set the entity category. | ||||
|   EntityCategory get_entity_category() const; | ||||
|   void set_entity_category(EntityCategory entity_category); | ||||
|   EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); } | ||||
|   void set_entity_category(EntityCategory entity_category) { | ||||
|     this->flags_.entity_category = static_cast<uint8_t>(entity_category); | ||||
|   } | ||||
|  | ||||
|   // Get/set this entity's icon | ||||
|   std::string get_icon() const; | ||||
|   void set_icon(const char *icon); | ||||
|  | ||||
|   // 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; } | ||||
|  | ||||
|  protected: | ||||
|   /// The hash_base() function has been deprecated. It is kept in this | ||||
|   /// class for now, to prevent external components from not compiling. | ||||
| @@ -59,11 +67,16 @@ class EntityBase { | ||||
|   const char *object_id_c_str_{nullptr}; | ||||
|   const char *icon_c_str_{nullptr}; | ||||
|   uint32_t object_id_hash_{}; | ||||
|   bool has_own_name_{false}; | ||||
|   bool internal_{false}; | ||||
|   bool disabled_by_default_{false}; | ||||
|   EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; | ||||
|   bool has_state_{}; | ||||
|  | ||||
|   // 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_{}; | ||||
| }; | ||||
|  | ||||
| class EntityBase_DeviceClass {  // NOLINT(readability-identifier-naming) | ||||
|   | ||||
							
								
								
									
										108
									
								
								tests/integration/fixtures/host_mode_entity_fields.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								tests/integration/fixtures/host_mode_entity_fields.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| esphome: | ||||
|   name: host-test | ||||
|  | ||||
| host: | ||||
|  | ||||
| api: | ||||
|  | ||||
| logger: | ||||
|  | ||||
| # Test various entity types with different flag combinations | ||||
| sensor: | ||||
|   - platform: template | ||||
|     name: "Test Normal Sensor" | ||||
|     id: normal_sensor | ||||
|     update_interval: 1s | ||||
|     lambda: |- | ||||
|       return 42.0; | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Test Internal Sensor" | ||||
|     id: internal_sensor | ||||
|     internal: true | ||||
|     update_interval: 1s | ||||
|     lambda: |- | ||||
|       return 43.0; | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Test Disabled Sensor" | ||||
|     id: disabled_sensor | ||||
|     disabled_by_default: true | ||||
|     update_interval: 1s | ||||
|     lambda: |- | ||||
|       return 44.0; | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Test Mixed Flags Sensor" | ||||
|     id: mixed_flags_sensor | ||||
|     internal: true | ||||
|     entity_category: diagnostic | ||||
|     update_interval: 1s | ||||
|     lambda: |- | ||||
|       return 45.0; | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Test Diagnostic Sensor" | ||||
|     id: diagnostic_sensor | ||||
|     entity_category: diagnostic | ||||
|     update_interval: 1s | ||||
|     lambda: |- | ||||
|       return 46.0; | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Test All Flags Sensor" | ||||
|     id: all_flags_sensor | ||||
|     internal: true | ||||
|     disabled_by_default: true | ||||
|     entity_category: diagnostic | ||||
|     update_interval: 1s | ||||
|     lambda: |- | ||||
|       return 47.0; | ||||
|  | ||||
| # Also test other entity types to ensure bit-packing works across all | ||||
| binary_sensor: | ||||
|   - platform: template | ||||
|     name: "Test Binary Sensor" | ||||
|     entity_category: config | ||||
|     lambda: |- | ||||
|       return true; | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: template | ||||
|     name: "Test Text Sensor" | ||||
|     disabled_by_default: true | ||||
|     lambda: |- | ||||
|       return {"Hello"}; | ||||
|  | ||||
| number: | ||||
|   - platform: template | ||||
|     name: "Test Number" | ||||
|     initial_value: 50 | ||||
|     min_value: 0 | ||||
|     max_value: 100 | ||||
|     step: 1 | ||||
|     optimistic: true | ||||
|     entity_category: diagnostic | ||||
|  | ||||
| select: | ||||
|   - platform: template | ||||
|     name: "Test Select" | ||||
|     options: | ||||
|       - "Option 1" | ||||
|       - "Option 2" | ||||
|     initial_option: "Option 1" | ||||
|     optimistic: true | ||||
|     internal: true | ||||
|  | ||||
| switch: | ||||
|   - platform: template | ||||
|     name: "Test Switch" | ||||
|     optimistic: true | ||||
|     disabled_by_default: true | ||||
|     entity_category: config | ||||
|  | ||||
| button: | ||||
|   - platform: template | ||||
|     name: "Test Button" | ||||
|     on_press: | ||||
|       - logger.log: "Button pressed" | ||||
							
								
								
									
										93
									
								
								tests/integration/test_host_mode_entity_fields.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								tests/integration/test_host_mode_entity_fields.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| """Integration test for entity bit-packed fields.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
|  | ||||
| from aioesphomeapi import EntityCategory, EntityState | ||||
| import pytest | ||||
|  | ||||
| from .types import APIClientConnectedFactory, RunCompiledFunction | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_host_mode_entity_fields( | ||||
|     yaml_config: str, | ||||
|     run_compiled: RunCompiledFunction, | ||||
|     api_client_connected: APIClientConnectedFactory, | ||||
| ) -> None: | ||||
|     """Test entity bit-packed fields work correctly with all possible values.""" | ||||
|     # Write, compile and run the ESPHome device, then connect to API | ||||
|     async with run_compiled(yaml_config), api_client_connected() as client: | ||||
|         # Get all entities | ||||
|         entities = await client.list_entities_services() | ||||
|  | ||||
|         # Create a map of entity names to entity info | ||||
|         entity_map = {} | ||||
|         for entity in entities[0]: | ||||
|             if hasattr(entity, "name"): | ||||
|                 entity_map[entity.name] = entity | ||||
|  | ||||
|         # Test entities that should be visible via API (non-internal) | ||||
|         visible_test_cases = [ | ||||
|             # (entity_name, expected_disabled_by_default, expected_entity_category) | ||||
|             ("Test Normal Sensor", False, EntityCategory.NONE), | ||||
|             ("Test Disabled Sensor", True, EntityCategory.NONE), | ||||
|             ("Test Diagnostic Sensor", False, EntityCategory.DIAGNOSTIC), | ||||
|             ("Test Switch", True, EntityCategory.CONFIG), | ||||
|             ("Test Binary Sensor", False, EntityCategory.CONFIG), | ||||
|             ("Test Number", False, EntityCategory.DIAGNOSTIC), | ||||
|         ] | ||||
|  | ||||
|         # Test entities that should NOT be visible via API (internal) | ||||
|         internal_entities = [ | ||||
|             "Test Internal Sensor", | ||||
|             "Test Mixed Flags Sensor", | ||||
|             "Test All Flags Sensor", | ||||
|             "Test Select", | ||||
|         ] | ||||
|  | ||||
|         # Verify visible entities | ||||
|         for entity_name, expected_disabled, expected_category in visible_test_cases: | ||||
|             assert entity_name in entity_map, ( | ||||
|                 f"Entity '{entity_name}' not found - it should be visible via API" | ||||
|             ) | ||||
|             entity = entity_map[entity_name] | ||||
|  | ||||
|             # Check disabled_by_default flag | ||||
|             assert entity.disabled_by_default == expected_disabled, ( | ||||
|                 f"{entity_name}: disabled_by_default flag mismatch - " | ||||
|                 f"expected {expected_disabled}, got {entity.disabled_by_default}" | ||||
|             ) | ||||
|  | ||||
|             # Check entity_category | ||||
|             assert entity.entity_category == expected_category, ( | ||||
|                 f"{entity_name}: entity_category mismatch - " | ||||
|                 f"expected {expected_category}, got {entity.entity_category}" | ||||
|             ) | ||||
|  | ||||
|         # Verify internal entities are NOT visible | ||||
|         for entity_name in internal_entities: | ||||
|             assert entity_name not in entity_map, ( | ||||
|                 f"Entity '{entity_name}' found in API response - " | ||||
|                 f"internal entities should not be exposed via API" | ||||
|             ) | ||||
|  | ||||
|         # Subscribe to states to verify has_state flag works | ||||
|         states: dict[int, EntityState] = {} | ||||
|         state_received = asyncio.Event() | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
|             states[state.key] = state | ||||
|             state_received.set() | ||||
|  | ||||
|         client.subscribe_states(on_state) | ||||
|  | ||||
|         # Wait for at least one state | ||||
|         try: | ||||
|             await asyncio.wait_for(state_received.wait(), timeout=5.0) | ||||
|         except asyncio.TimeoutError: | ||||
|             pytest.fail("No states received within 5 seconds") | ||||
|  | ||||
|         # Verify we received states (which means has_state flag is working) | ||||
|         assert len(states) > 0, "No states received - has_state flag may not be working" | ||||
		Reference in New Issue
	
	Block a user