mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Reduce entity memory usage by eliminating field shadowing and bit-packing (#9076)
This commit is contained in:
		
				
					committed by
					
						 Jesse Hills
						Jesse Hills
					
				
			
			
				
	
			
			
			
						parent
						
							fb12e4e66a
						
					
				
				
					commit
					4787e22f61
				
			| @@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity"; | |||||||
|  |  | ||||||
| void DateEntity::publish_state() { | void DateEntity::publish_state() { | ||||||
|   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { |   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->year_ < 1970 || this->year_ > 3000) { |   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"); |     ESP_LOGE(TAG, "Year must be between 1970 and 3000"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->month_ < 1 || this->month_ > 12) { |   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"); |     ESP_LOGE(TAG, "Month must be between 1 and 12"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->day_ > days_in_month(this->month_, this->year_)) { |   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_); |     ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); | ||||||
|     return; |     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_); |   ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); | ||||||
|   this->state_callback_.call(); |   this->state_callback_.call(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,9 +13,6 @@ namespace datetime { | |||||||
|  |  | ||||||
| class DateTimeBase : public EntityBase { | class DateTimeBase : public EntityBase { | ||||||
|  public: |  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; |   virtual ESPTime state_as_esptime() const = 0; | ||||||
|  |  | ||||||
|   void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } |   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 | #ifdef USE_TIME | ||||||
|   time::RealTimeClock *rtc_; |   time::RealTimeClock *rtc_; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   bool has_state_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #ifdef USE_TIME | #ifdef USE_TIME | ||||||
|   | |||||||
| @@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity"; | |||||||
|  |  | ||||||
| void DateTimeEntity::publish_state() { | void DateTimeEntity::publish_state() { | ||||||
|   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { |   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->year_ < 1970 || this->year_ > 3000) { |   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"); |     ESP_LOGE(TAG, "Year must be between 1970 and 3000"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->month_ < 1 || this->month_ > 12) { |   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"); |     ESP_LOGE(TAG, "Month must be between 1 and 12"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->day_ > days_in_month(this->month_, this->year_)) { |   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_); |     ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->hour_ > 23) { |   if (this->hour_ > 23) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     ESP_LOGE(TAG, "Hour must be between 0 and 23"); |     ESP_LOGE(TAG, "Hour must be between 0 and 23"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->minute_ > 59) { |   if (this->minute_ > 59) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     ESP_LOGE(TAG, "Minute must be between 0 and 59"); |     ESP_LOGE(TAG, "Minute must be between 0 and 59"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->second_ > 59) { |   if (this->second_ > 59) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     ESP_LOGE(TAG, "Second must be between 0 and 59"); |     ESP_LOGE(TAG, "Second must be between 0 and 59"); | ||||||
|     return; |     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_, |   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->month_, this->day_, this->hour_, this->minute_, this->second_); | ||||||
|   this->state_callback_.call(); |   this->state_callback_.call(); | ||||||
|   | |||||||
| @@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity"; | |||||||
|  |  | ||||||
| void TimeEntity::publish_state() { | void TimeEntity::publish_state() { | ||||||
|   if (this->hour_ > 23) { |   if (this->hour_ > 23) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     ESP_LOGE(TAG, "Hour must be between 0 and 23"); |     ESP_LOGE(TAG, "Hour must be between 0 and 23"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->minute_ > 59) { |   if (this->minute_ > 59) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     ESP_LOGE(TAG, "Minute must be between 0 and 59"); |     ESP_LOGE(TAG, "Minute must be between 0 and 59"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->second_ > 59) { |   if (this->second_ > 59) { | ||||||
|     this->has_state_ = false; |     this->set_has_state(false); | ||||||
|     ESP_LOGE(TAG, "Second must be between 0 and 59"); |     ESP_LOGE(TAG, "Second must be between 0 and 59"); | ||||||
|     return; |     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_, |   ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, | ||||||
|            this->second_); |            this->second_); | ||||||
|   this->state_callback_.call(); |   this->state_callback_.call(); | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ void ESP32Camera::dump_config() { | |||||||
|                 "  External Clock: Pin:%d Frequency:%u\n" |                 "  External Clock: Pin:%d Frequency:%u\n" | ||||||
|                 "  I2C Pins: SDA:%d SCL:%d\n" |                 "  I2C Pins: SDA:%d SCL:%d\n" | ||||||
|                 "  Reset Pin: %d", |                 "  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_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); |                 conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); | ||||||
|   switch (this->config_.frame_size) { |   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); |     this->publish_state(state); | ||||||
|   } else { |   } else { | ||||||
|     this->state = state; |     this->state = state; | ||||||
|     this->has_state_ = true; |     this->set_has_state(true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->update_component_settings(); |   this->update_component_settings(); | ||||||
|   | |||||||
| @@ -88,7 +88,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { | |||||||
|     } else { |     } else { | ||||||
|       this->raw_state = state; |       this->raw_state = state; | ||||||
|       this->state = state; |       this->state = state; | ||||||
|       this->has_state_ = true; |       this->set_has_state(true); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   this->update_component_settings(); |   this->update_component_settings(); | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s | |||||||
|     this->publish_state(state); |     this->publish_state(state); | ||||||
|   } else { |   } else { | ||||||
|     this->state = state; |     this->state = state; | ||||||
|     this->has_state_ = true; |     this->set_has_state(true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->update_component_settings(); |   this->update_component_settings(); | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace number { | |||||||
| static const char *const TAG = "number"; | static const char *const TAG = "number"; | ||||||
|  |  | ||||||
| void Number::publish_state(float state) { | void Number::publish_state(float state) { | ||||||
|   this->has_state_ = true; |   this->set_has_state(true); | ||||||
|   this->state = state; |   this->state = state; | ||||||
|   ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); |   ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); | ||||||
|   this->state_callback_.call(state); |   this->state_callback_.call(state); | ||||||
|   | |||||||
| @@ -48,9 +48,6 @@ class Number : public EntityBase { | |||||||
|  |  | ||||||
|   NumberTraits traits; |   NumberTraits traits; | ||||||
|  |  | ||||||
|   /// Return whether this number has gotten a full state yet. |  | ||||||
|   bool has_state() const { return has_state_; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   friend class NumberCall; |   friend class NumberCall; | ||||||
|  |  | ||||||
| @@ -63,7 +60,6 @@ class Number : public EntityBase { | |||||||
|   virtual void control(float value) = 0; |   virtual void control(float value) = 0; | ||||||
|  |  | ||||||
|   CallbackManager<void(float)> state_callback_; |   CallbackManager<void(float)> state_callback_; | ||||||
|   bool has_state_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace number | }  // namespace number | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ void Select::publish_state(const std::string &state) { | |||||||
|   auto index = this->index_of(state); |   auto index = this->index_of(state); | ||||||
|   const auto *name = this->get_name().c_str(); |   const auto *name = this->get_name().c_str(); | ||||||
|   if (index.has_value()) { |   if (index.has_value()) { | ||||||
|     this->has_state_ = true; |     this->set_has_state(true); | ||||||
|     this->state = state; |     this->state = state; | ||||||
|     ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); |     ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); | ||||||
|     this->state_callback_.call(state, index.value()); |     this->state_callback_.call(state, index.value()); | ||||||
|   | |||||||
| @@ -35,9 +35,6 @@ class Select : public EntityBase { | |||||||
|  |  | ||||||
|   void publish_state(const std::string &state); |   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. |   /// Instantiate a SelectCall object to modify this select component's state. | ||||||
|   SelectCall make_call() { return SelectCall(this); } |   SelectCall make_call() { return SelectCall(this); } | ||||||
|  |  | ||||||
| @@ -73,7 +70,6 @@ class Select : public EntityBase { | |||||||
|   virtual void control(const std::string &value) = 0; |   virtual void control(const std::string &value) = 0; | ||||||
|  |  | ||||||
|   CallbackManager<void(std::string, size_t)> state_callback_; |   CallbackManager<void(std::string, size_t)> state_callback_; | ||||||
|   bool has_state_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace select | }  // namespace select | ||||||
|   | |||||||
| @@ -88,13 +88,12 @@ float Sensor::get_raw_state() const { return this->raw_state; } | |||||||
| std::string Sensor::unique_id() { return ""; } | std::string Sensor::unique_id() { return ""; } | ||||||
|  |  | ||||||
| void Sensor::internal_send_state_to_frontend(float state) { | void Sensor::internal_send_state_to_frontend(float state) { | ||||||
|   this->has_state_ = true; |   this->set_has_state(true); | ||||||
|   this->state = state; |   this->state = state; | ||||||
|   ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), 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->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); | ||||||
|   this->callback_.call(state); |   this->callback_.call(state); | ||||||
| } | } | ||||||
| bool Sensor::has_state() const { return this->has_state_; } |  | ||||||
|  |  | ||||||
| }  // namespace sensor | }  // namespace sensor | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -140,9 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|    */ |    */ | ||||||
|   float raw_state; |   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. |   /** 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). |    * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). | ||||||
| @@ -160,7 +157,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override |   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override | ||||||
|   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override |   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override | ||||||
|   bool force_update_{false};                            ///< Force update mode |   bool force_update_{false};                            ///< Force update mode | ||||||
|   bool has_state_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace sensor | }  // namespace sensor | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace text { | |||||||
| static const char *const TAG = "text"; | static const char *const TAG = "text"; | ||||||
|  |  | ||||||
| void Text::publish_state(const std::string &state) { | void Text::publish_state(const std::string &state) { | ||||||
|   this->has_state_ = true; |   this->set_has_state(true); | ||||||
|   this->state = state; |   this->state = state; | ||||||
|   if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { |   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()); |     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); |   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. |   /// Instantiate a TextCall object to modify this text component's state. | ||||||
|   TextCall make_call() { return TextCall(this); } |   TextCall make_call() { return TextCall(this); } | ||||||
|  |  | ||||||
| @@ -48,7 +45,6 @@ class Text : public EntityBase { | |||||||
|   virtual void control(const std::string &value) = 0; |   virtual void control(const std::string &value) = 0; | ||||||
|  |  | ||||||
|   CallbackManager<void(std::string)> state_callback_; |   CallbackManager<void(std::string)> state_callback_; | ||||||
|   bool has_state_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace text | }  // namespace text | ||||||
|   | |||||||
| @@ -60,13 +60,12 @@ std::string TextSensor::get_state() const { return this->state; } | |||||||
| std::string TextSensor::get_raw_state() const { return this->raw_state; } | std::string TextSensor::get_raw_state() const { return this->raw_state; } | ||||||
| void TextSensor::internal_send_state_to_frontend(const std::string &state) { | void TextSensor::internal_send_state_to_frontend(const std::string &state) { | ||||||
|   this->state = 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()); |   ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); | ||||||
|   this->callback_.call(state); |   this->callback_.call(state); | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string TextSensor::unique_id() { return ""; } | std::string TextSensor::unique_id() { return ""; } | ||||||
| bool TextSensor::has_state() { return this->has_state_; } |  | ||||||
|  |  | ||||||
| }  // namespace text_sensor | }  // namespace text_sensor | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -67,8 +67,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { | |||||||
|    */ |    */ | ||||||
|   virtual std::string unique_id(); |   virtual std::string unique_id(); | ||||||
|  |  | ||||||
|   bool has_state(); |  | ||||||
|  |  | ||||||
|   void internal_send_state_to_frontend(const std::string &state); |   void internal_send_state_to_frontend(const std::string &state); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -76,8 +74,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { | |||||||
|   CallbackManager<void(std::string)> callback_;      ///< Storage for filtered state callbacks. |   CallbackManager<void(std::string)> callback_;      ///< Storage for filtered state callbacks. | ||||||
|  |  | ||||||
|   Filter *filter_list_{nullptr};  ///< Store all active filters. |   Filter *filter_list_{nullptr};  ///< Store all active filters. | ||||||
|  |  | ||||||
|   bool has_state_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace text_sensor | }  // namespace text_sensor | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ void UpdateEntity::publish_state() { | |||||||
|     ESP_LOGD(TAG, "  Progress: %.0f%%", this->update_info_.progress); |     ESP_LOGD(TAG, "  Progress: %.0f%%", this->update_info_.progress); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->has_state_ = true; |   this->set_has_state(true); | ||||||
|   this->state_callback_.call(); |   this->state_callback_.call(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,8 +28,6 @@ enum UpdateState : uint8_t { | |||||||
|  |  | ||||||
| class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { | class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { | ||||||
|  public: |  public: | ||||||
|   bool has_state() const { return this->has_state_; } |  | ||||||
|  |  | ||||||
|   void publish_state(); |   void publish_state(); | ||||||
|  |  | ||||||
|   void perform() { this->perform(false); } |   void perform() { this->perform(false); } | ||||||
| @@ -44,7 +42,6 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { | |||||||
|  protected: |  protected: | ||||||
|   UpdateState state_{UPDATE_STATE_UNKNOWN}; |   UpdateState state_{UPDATE_STATE_UNKNOWN}; | ||||||
|   UpdateInfo update_info_; |   UpdateInfo update_info_; | ||||||
|   bool has_state_{false}; |  | ||||||
|  |  | ||||||
|   CallbackManager<void()> state_callback_{}; |   CallbackManager<void()> state_callback_{}; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ static const char *const TAG = "uptime.sensor"; | |||||||
|  |  | ||||||
| void UptimeTimestampSensor::setup() { | void UptimeTimestampSensor::setup() { | ||||||
|   this->time_->add_on_time_sync_callback([this]() { |   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 |       return;  // No need to update the timestamp if it's already set | ||||||
|  |  | ||||||
|     auto now = this->time_->now(); |     auto now = this->time_->now(); | ||||||
|   | |||||||
| @@ -12,20 +12,12 @@ void EntityBase::set_name(const char *name) { | |||||||
|   this->name_ = StringRef(name); |   this->name_ = StringRef(name); | ||||||
|   if (this->name_.empty()) { |   if (this->name_.empty()) { | ||||||
|     this->name_ = StringRef(App.get_friendly_name()); |     this->name_ = StringRef(App.get_friendly_name()); | ||||||
|     this->has_own_name_ = false; |     this->flags_.has_own_name = false; | ||||||
|   } else { |   } 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 | // Entity Icon | ||||||
| std::string EntityBase::get_icon() const { | std::string EntityBase::get_icon() const { | ||||||
|   if (this->icon_c_str_ == nullptr) { |   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; } | 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 | // Entity Object ID | ||||||
| std::string EntityBase::get_object_id() const { | std::string EntityBase::get_object_id() const { | ||||||
|   // Check if `App.get_friendly_name()` is constant or dynamic. |   // 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. |     // `App.get_friendly_name()` is dynamic. | ||||||
|     return str_sanitize(str_snake_case(App.get_friendly_name())); |     return str_sanitize(str_snake_case(App.get_friendly_name())); | ||||||
|   } else { |   } else { | ||||||
| @@ -61,7 +49,7 @@ void EntityBase::set_object_id(const char *object_id) { | |||||||
| // Calculate Object ID Hash from Entity Name | // Calculate Object ID Hash from Entity Name | ||||||
| void EntityBase::calc_object_id_() { | void EntityBase::calc_object_id_() { | ||||||
|   // Check if `App.get_friendly_name()` is constant or dynamic. |   // 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. |     // `App.get_friendly_name()` is dynamic. | ||||||
|     const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); |     const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); | ||||||
|     // FNV-1 hash |     // FNV-1 hash | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ class EntityBase { | |||||||
|   void set_name(const char *name); |   void set_name(const char *name); | ||||||
|  |  | ||||||
|   // Get whether this Entity has its own name or it should use the device friendly_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. |   // Get the sanitized name of this Entity as an ID. | ||||||
|   std::string get_object_id() const; |   std::string get_object_id() const; | ||||||
| @@ -29,24 +29,32 @@ class EntityBase { | |||||||
|   // Get the unique Object ID of this Entity |   // Get the unique Object ID of this Entity | ||||||
|   uint32_t get_object_id_hash(); |   uint32_t get_object_id_hash(); | ||||||
|  |  | ||||||
|   // Get/set whether this Entity should be hidden from outside of ESPHome |   // Get/set whether this Entity should be hidden outside ESPHome | ||||||
|   bool is_internal() const; |   bool is_internal() const { return this->flags_.internal; } | ||||||
|   void set_internal(bool internal); |   void set_internal(bool internal) { this->flags_.internal = internal; } | ||||||
|  |  | ||||||
|   // Check if this object is declared to be disabled by default. |   // 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 |   // 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. |   // 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; |   bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } | ||||||
|   void set_disabled_by_default(bool 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. |   // Get/set the entity category. | ||||||
|   EntityCategory get_entity_category() const; |   EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); } | ||||||
|   void set_entity_category(EntityCategory 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 |   // Get/set this entity's icon | ||||||
|   std::string get_icon() const; |   std::string get_icon() const; | ||||||
|   void set_icon(const char *icon); |   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: |  protected: | ||||||
|   /// The hash_base() function has been deprecated. It is kept in this |   /// The hash_base() function has been deprecated. It is kept in this | ||||||
|   /// class for now, to prevent external components from not compiling. |   /// class for now, to prevent external components from not compiling. | ||||||
| @@ -56,11 +64,17 @@ class EntityBase { | |||||||
|   StringRef name_; |   StringRef name_; | ||||||
|   const char *object_id_c_str_{nullptr}; |   const char *object_id_c_str_{nullptr}; | ||||||
|   const char *icon_c_str_{nullptr}; |   const char *icon_c_str_{nullptr}; | ||||||
|   uint32_t object_id_hash_; |   uint32_t object_id_hash_{}; | ||||||
|   bool has_own_name_{false}; |  | ||||||
|   bool internal_{false}; |   // Bit-packed flags to save memory (1 byte instead of 5) | ||||||
|   bool disabled_by_default_{false}; |   struct EntityFlags { | ||||||
|   EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; |     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) | 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