mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -12,6 +12,25 @@ | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| // IDF version 5.5.1 and above is required because of a bug in | ||||
| // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 | ||||
| typedef union {  // NOLINT(modernize-use-using) | ||||
|   struct { | ||||
|     uint16_t duration : 15; | ||||
|     uint16_t level : 1; | ||||
|   }; | ||||
|   uint16_t val; | ||||
| } rmt_symbol_half_t; | ||||
|  | ||||
| struct RemoteTransmitterComponentStore { | ||||
|   uint32_t times{0}; | ||||
|   uint32_t index{0}; | ||||
| }; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|                                    public Component | ||||
| #ifdef USE_ESP32 | ||||
| @@ -56,9 +75,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   RemoteTransmitterComponentStore store_{}; | ||||
|   std::vector<rmt_symbol_half_t> rmt_temp_; | ||||
| #else | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
| #endif | ||||
|   uint32_t current_carrier_frequency_{38000}; | ||||
|   bool initialized_{false}; | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
|   bool with_dma_{false}; | ||||
|   bool eot_level_{false}; | ||||
|   rmt_channel_handle_t channel_{NULL}; | ||||
|   | ||||
| @@ -10,6 +10,46 @@ namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| // Maximum RMT symbol duration (15-bit field) | ||||
| static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free, | ||||
|                                              rmt_symbol_word_t *symbols, bool *done, void *arg) { | ||||
|   auto *store = static_cast<RemoteTransmitterComponentStore *>(arg); | ||||
|   const auto *encoded = static_cast<const rmt_symbol_half_t *>(data); | ||||
|   size_t length = size / sizeof(rmt_symbol_half_t); | ||||
|   size_t count = 0; | ||||
|  | ||||
|   // copy symbols | ||||
|   for (size_t i = 0; i < free; i++) { | ||||
|     uint16_t sym_0 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0; | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     uint16_t sym_1 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|   } | ||||
|   *done = false; | ||||
|   return count; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->inverted_ = this->pin_->is_inverted(); | ||||
|   this->configure_rmt_(); | ||||
| @@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() { | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   rmt_symbol_half_t symbol = { | ||||
|       .duration = 1, | ||||
|       .level = value, | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = value; | ||||
|   this->store_.times = 1; | ||||
|   this->store_.index = 0; | ||||
| #else | ||||
|   rmt_symbol_word_t symbol = { | ||||
|       .duration0 = 1, | ||||
|       .level0 = value, | ||||
| @@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.loop_count = 0; | ||||
|   config.flags.eot_level = value; | ||||
| #endif | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
| @@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); | ||||
|     } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|     rmt_simple_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     encoder.callback = encoder_callback; | ||||
|     encoder.arg = &this->store_; | ||||
|     encoder.min_chunk_size = 1; | ||||
|     error = rmt_new_simple_encoder(&encoder, &this->encoder_); | ||||
|     if (error != ESP_OK) { | ||||
|       this->error_code_ = error; | ||||
|       this->error_string_ = "in rmt_new_simple_encoder"; | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #else | ||||
|     rmt_copy_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     error = rmt_new_copy_encoder(&encoder, &this->encoder_); | ||||
| @@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     error = rmt_enable(this->channel_); | ||||
|     if (error != ESP_OK) { | ||||
| @@ -130,6 +196,79 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { | ||||
|     this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); | ||||
|     this->configure_rmt_(); | ||||
|   } | ||||
|  | ||||
|   this->rmt_temp_.clear(); | ||||
|   this->rmt_temp_.reserve(this->temp_.get_data().size() + 1); | ||||
|  | ||||
|   // encode any delay at the start of the buffer to simplify the encoder callback | ||||
|   // this will be skipped the first time around | ||||
|   send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait)); | ||||
|   while (send_wait > 0) { | ||||
|     int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|     this->rmt_temp_.push_back({ | ||||
|         .duration = static_cast<uint16_t>(duration), | ||||
|         .level = static_cast<uint16_t>(this->eot_level_), | ||||
|     }); | ||||
|     send_wait -= duration; | ||||
|   } | ||||
|  | ||||
|   // encode data | ||||
|   size_t offset = this->rmt_temp_.size(); | ||||
|   for (int32_t value : this->temp_.get_data()) { | ||||
|     bool level = value >= 0; | ||||
|     if (!level) { | ||||
|       value = -value; | ||||
|     } | ||||
|     value = this->from_microseconds_(static_cast<uint32_t>(value)); | ||||
|     while (value > 0) { | ||||
|       int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       this->rmt_temp_.push_back({ | ||||
|           .duration = static_cast<uint16_t>(duration), | ||||
|           .level = static_cast<uint16_t>(level ^ this->inverted_), | ||||
|       }); | ||||
|       value -= duration; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) { | ||||
|     ESP_LOGE(TAG, "Empty data"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->transmit_trigger_->trigger(); | ||||
|  | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = this->eot_level_; | ||||
|   this->store_.times = send_times; | ||||
|   this->store_.index = offset; | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                  this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } else { | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|   error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #else | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) | ||||
|     return; | ||||
| @@ -151,7 +290,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|     val = this->from_microseconds_(static_cast<uint32_t>(val)); | ||||
|  | ||||
|     do { | ||||
|       int32_t item = std::min(val, int32_t(32767)); | ||||
|       int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       val -= item; | ||||
|  | ||||
|       if (rmt_i % 2 == 0) { | ||||
| @@ -180,7 +319,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     rmt_transmit_config_t config; | ||||
|     memset(&config, 0, sizeof(config)); | ||||
|     config.loop_count = 0; | ||||
|     config.flags.eot_level = this->eot_level_; | ||||
|     esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                    this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); | ||||
| @@ -200,6 +338,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -74,7 +74,8 @@ async def to_code(config): | ||||
|  | ||||
|     else: | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) | ||||
|         initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) | ||||
|         cg.add(var.set_initial_option_index(initial_option_index)) | ||||
|  | ||||
|         if CONF_RESTORE_VALUE in config: | ||||
|             cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) | ||||
|   | ||||
| @@ -10,26 +10,21 @@ void TemplateSelect::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   std::string value; | ||||
|   if (!this->restore_value_) { | ||||
|     value = this->initial_option_; | ||||
|     ESP_LOGD(TAG, "State from initial: %s", value.c_str()); | ||||
|   } else { | ||||
|     size_t index; | ||||
|   size_t index = this->initial_option_index_; | ||||
|   if (this->restore_value_) { | ||||
|     this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash()); | ||||
|     if (!this->pref_.load(&index)) { | ||||
|       value = this->initial_option_; | ||||
|       ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); | ||||
|     } else if (!this->has_index(index)) { | ||||
|       value = this->initial_option_; | ||||
|       ESP_LOGD(TAG, "State from initial (restored index %zu out of bounds): %s", index, value.c_str()); | ||||
|     size_t restored_index; | ||||
|     if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { | ||||
|       index = restored_index; | ||||
|       ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str()); | ||||
|     } else { | ||||
|       value = this->at(index).value(); | ||||
|       ESP_LOGD(TAG, "State from restore: %s", value.c_str()); | ||||
|       ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); | ||||
|   } | ||||
|  | ||||
|   this->publish_state(value); | ||||
|   this->publish_state(this->at(index).value()); | ||||
| } | ||||
|  | ||||
| void TemplateSelect::update() { | ||||
| @@ -65,11 +60,14 @@ void TemplateSelect::dump_config() { | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|   auto initial_option = this->at(this->initial_option_index_); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Optimistic: %s\n" | ||||
|                 "  Initial Option: %s\n" | ||||
|                 "  Restore Value: %s", | ||||
|                 YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_)); | ||||
|                 YESNO(this->optimistic_), | ||||
|                 initial_option.has_value() ? initial_option.value().c_str() : LOG_STR_LITERAL("unknown"), | ||||
|                 YESNO(this->restore_value_)); | ||||
| } | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -19,13 +19,13 @@ class TemplateSelect : public select::Select, public PollingComponent { | ||||
|  | ||||
|   Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; } | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|   void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; } | ||||
|   void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } | ||||
|   void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   bool optimistic_ = false; | ||||
|   std::string initial_option_; | ||||
|   size_t initial_option_index_{0}; | ||||
|   bool restore_value_ = false; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<std::function<optional<std::string>()>> f_; | ||||
|   | ||||
| @@ -41,6 +41,17 @@ select: | ||||
|       - ""  # Empty string at the end | ||||
|     initial_option: "Choice X" | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Select Initial Option Test" | ||||
|     id: select_initial_option_test | ||||
|     optimistic: true | ||||
|     options: | ||||
|       - "First" | ||||
|       - "Second" | ||||
|       - "Third" | ||||
|       - "Fourth" | ||||
|     initial_option: "Third"  # Test non-default initial option | ||||
|  | ||||
| # Add a sensor to ensure we have other entities in the list | ||||
| sensor: | ||||
|   - platform: template | ||||
|   | ||||
| @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( | ||||
|  | ||||
|         # Find our select entities | ||||
|         select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] | ||||
|         assert len(select_entities) == 3, ( | ||||
|             f"Expected 3 select entities, got {len(select_entities)}" | ||||
|         assert len(select_entities) == 4, ( | ||||
|             f"Expected 4 select entities, got {len(select_entities)}" | ||||
|         ) | ||||
|  | ||||
|         # Verify each select entity by name and check their options | ||||
| @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_last.options[2] == "Choice Z" | ||||
|         assert empty_last.options[3] == ""  # Empty string at end | ||||
|  | ||||
|         # Check "Select Initial Option Test" - verify non-default initial option | ||||
|         assert "Select Initial Option Test" in selects_by_name | ||||
|         initial_option_test = selects_by_name["Select Initial Option Test"] | ||||
|         assert len(initial_option_test.options) == 4 | ||||
|         assert initial_option_test.options[0] == "First" | ||||
|         assert initial_option_test.options[1] == "Second" | ||||
|         assert initial_option_test.options[2] == "Third" | ||||
|         assert initial_option_test.options[3] == "Fourth" | ||||
|  | ||||
|         # If we got here without protobuf decoding errors, the fix is working | ||||
|         # The bug would have caused "Invalid protobuf message" errors with trailing bytes | ||||
|  | ||||
| @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( | ||||
|         # This ensures empty strings work properly in state messages too | ||||
|         states: dict[int, EntityState] = {} | ||||
|         states_received_future: asyncio.Future[None] = loop.create_future() | ||||
|         expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} | ||||
|         expected_select_keys = { | ||||
|             empty_first.key, | ||||
|             empty_middle.key, | ||||
|             empty_last.key, | ||||
|             initial_option_test.key, | ||||
|         } | ||||
|         received_select_keys = set() | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
| @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_first.key in states | ||||
|         assert empty_middle.key in states | ||||
|         assert empty_last.key in states | ||||
|         assert initial_option_test.key in states | ||||
|  | ||||
|         # Verify the initial option is set correctly to "Third" (not the default "First") | ||||
|         initial_state = states[initial_option_test.key] | ||||
|         assert initial_state.state == "Third", ( | ||||
|             f"Expected initial state 'Third' but got '{initial_state.state}' - " | ||||
|             f"initial_option_index optimization may not be working correctly" | ||||
|         ) | ||||
|  | ||||
|         # The main test is that we got here without protobuf errors | ||||
|         # The select entities with empty string options were properly encoded | ||||
|   | ||||
		Reference in New Issue
	
	Block a user