From a92a08c2de7736369f533679b0685e07ad59d990 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 13:40:36 -0500 Subject: [PATCH 01/21] [api] Fix string lifetime issue in fill_and_encode_entity_info for dynamic object_id (#10482) --- esphome/components/api/api_connection.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6254854238..72254d1536 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -303,11 +303,13 @@ class APIConnection final : public APIServerConnection { msg.key = entity->get_object_id_hash(); // Try to use static reference first to avoid allocation StringRef static_ref = entity->get_object_id_ref_for_api_(); + // Store dynamic string outside the if-else to maintain lifetime + std::string object_id; if (!static_ref.empty()) { msg.set_object_id(static_ref); } else { // Dynamic case - need to allocate - std::string object_id = entity->get_object_id(); + object_id = entity->get_object_id(); msg.set_object_id(StringRef(object_id)); } From 2f2f2f7d15b78cf9ed96fc9078dc3e2c9c2fb82e Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Thu, 28 Aug 2025 16:04:19 -0600 Subject: [PATCH 02/21] [absolute_humidity] Fix typo (#10474) --- .../components/absolute_humidity/absolute_humidity.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b8717ac5f1..7ba3c5a1ab 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -61,11 +61,10 @@ void AbsoluteHumidityComponent::loop() { ESP_LOGW(TAG, "No valid state from temperature sensor!"); } if (no_humidity) { - ESP_LOGW(TAG, "No valid state from temperature sensor!"); + ESP_LOGW(TAG, "No valid state from humidity sensor!"); } - ESP_LOGW(TAG, "Unable to calculate absolute humidity."); this->publish_state(NAN); - this->status_set_warning(); + this->status_set_warning("Unable to calculate absolute humidity."); return; } @@ -87,9 +86,8 @@ void AbsoluteHumidityComponent::loop() { es = es_wobus(temperature_c); break; default: - ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); this->publish_state(NAN); - this->status_set_error(); + this->status_set_error("Invalid saturation vapor pressure equation selection!"); return; } ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); From d4c11dac8c19e78be78efc3dd63096c07d39a9d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:12:38 -0500 Subject: [PATCH 03/21] [esphome] Fix OTA watchdog resets by validating all magic bytes before blocking (#10401) --- .../components/esphome/ota/ota_esphome.cpp | 77 ++++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 8 +- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 5217e9c61f..fc10e5366e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -100,8 +100,8 @@ void ESPHomeOTAComponent::handle_handshake_() { /// Handle the initial OTA handshake. /// /// This method is non-blocking and will return immediately if no data is available. - /// It waits for the first magic byte (0x6C) before proceeding to handle_data_(). - /// A 10-second timeout is enforced from initial connection. + /// It reads all 5 magic bytes (0x6C, 0x26, 0xF7, 0x5C, 0x45) non-blocking + /// before proceeding to handle_data_(). A 10-second timeout is enforced from initial connection. if (this->client_ == nullptr) { // We already checked server_->ready() in loop(), so we can accept directly @@ -126,6 +126,7 @@ void ESPHomeOTAComponent::handle_handshake_() { } this->log_start_("handshake"); this->client_connect_time_ = App.get_loop_component_start_time(); + this->magic_buf_pos_ = 0; // Reset magic buffer position } // Check for handshake timeout @@ -136,34 +137,47 @@ void ESPHomeOTAComponent::handle_handshake_() { return; } - // Try to read first byte of magic bytes - uint8_t first_byte; - ssize_t read = this->client_->read(&first_byte, 1); + // Try to read remaining magic bytes + if (this->magic_buf_pos_ < 5) { + // Read as many bytes as available + uint8_t bytes_to_read = 5 - this->magic_buf_pos_; + ssize_t read = this->client_->read(this->magic_buf_ + this->magic_buf_pos_, bytes_to_read); - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - return; // No data yet, try again next loop - } - - if (read <= 0) { - // Error or connection closed - if (read == -1) { - this->log_socket_error_("reading first byte"); - } else { - ESP_LOGW(TAG, "Remote closed during handshake"); + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop } - this->cleanup_connection_(); - return; + + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_("reading magic bytes"); + } else { + ESP_LOGW(TAG, "Remote closed during handshake"); + } + this->cleanup_connection_(); + return; + } + + this->magic_buf_pos_ += read; } - // Got first byte, check if it's the magic byte - if (first_byte != 0x6C) { - ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); - this->cleanup_connection_(); - return; - } + // Check if we have all 5 magic bytes + if (this->magic_buf_pos_ == 5) { + // Validate magic bytes + static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + if (memcmp(this->magic_buf_, MAGIC_BYTES, 5) != 0) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->magic_buf_[0], + this->magic_buf_[1], this->magic_buf_[2], this->magic_buf_[3], this->magic_buf_[4]); + // Send error response (non-blocking, best effort) + uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); + this->client_->write(&error, 1); + this->cleanup_connection_(); + return; + } - // First byte is valid, continue with data handling - this->handle_data_(); + // All 5 magic bytes are valid, continue with data handling + this->handle_data_(); + } } void ESPHomeOTAComponent::handle_data_() { @@ -186,18 +200,6 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif - // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) - if (!this->readall_(buf, 4)) { - this->log_read_error_("magic bytes"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 - if (buf[0] != 0x26 || buf[1] != 0xF7 || buf[2] != 0x5C || buf[3] != 0x45) { - ESP_LOGW(TAG, "Magic bytes mismatch! 0x6C-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3]); - error_code = ota::OTA_RESPONSE_ERROR_MAGIC; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // Send OK and version - 2 bytes buf[0] = ota::OTA_RESPONSE_OK; buf[1] = USE_OTA_VERSION; @@ -487,6 +489,7 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; this->client_connect_time_ = 0; + this->magic_buf_pos_ = 0; } void ESPHomeOTAComponent::yield_and_feed_watchdog_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 8397b86528..c1919c71e9 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -41,11 +41,13 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::string password_; #endif // USE_OTA_PASSWORD - uint16_t port_; - uint32_t client_connect_time_{0}; - std::unique_ptr server_; std::unique_ptr client_; + + uint32_t client_connect_time_{0}; + uint16_t port_; + uint8_t magic_buf_[5]; + uint8_t magic_buf_pos_{0}; }; } // namespace esphome From a7786b75a0670898d189b2cebd2160c69fc0fb64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:14:51 -0500 Subject: [PATCH 04/21] [esp32_ble_tracker] Remove duplicate client promotion logic (#10321) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 32 ++----------------- .../esp32_ble_tracker/esp32_ble_tracker.h | 7 +--- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0455d136df..00bd1fe34c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -307,14 +307,7 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { // Process the scan result immediately - bool found_discovered_client = this->process_scan_result_(scan_result); - - // If we found a discovered client that needs promotion, stop scanning - // This replaces the promote_to_connecting logic from loop() - if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGD(TAG, "Found discovered client, stopping scan for connection"); - this->stop_scan_(); - } + this->process_scan_result_(scan_result); } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { // Scan finished on its own if (this->scanner_state_ != ScannerState::RUNNING) { @@ -720,20 +713,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); } -bool ESP32BLETracker::has_connecting_clients_() const { - for (auto *client : this->clients_) { - auto state = client->state(); - if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) { - return true; - } - } - return false; -} #endif // USE_ESP32_BLE_DEVICE -bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { - bool found_discovered_client = false; - +void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { // Process raw advertisements if (this->raw_advertisements_) { for (auto *listener : this->listeners_) { @@ -759,14 +741,6 @@ bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { for (auto *client : this->clients_) { if (client->parse_device(device)) { found = true; - // Check if this client is discovered and needs promotion - if (client->state() == ClientState::DISCOVERED) { - // Only check for connecting clients if we found a discovered client - // This matches the original logic: !connecting && client->state() == DISCOVERED - if (!this->has_connecting_clients_()) { - found_discovered_client = true; - } - } } } @@ -775,8 +749,6 @@ bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { } #endif // USE_ESP32_BLE_DEVICE } - - return found_discovered_client; } void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 3022eb25d2..763fa9f1c6 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -292,12 +292,7 @@ class ESP32BLETracker : public Component, /// Common cleanup logic when transitioning scanner to IDLE state void cleanup_scan_state_(bool is_stop_complete); /// Process a single scan result immediately - /// Returns true if a discovered client needs promotion to READY_TO_CONNECT - bool process_scan_result_(const BLEScanResult &scan_result); -#ifdef USE_ESP32_BLE_DEVICE - /// Check if any clients are in connecting or ready to connect state - bool has_connecting_clients_() const; -#endif + void process_scan_result_(const BLEScanResult &scan_result); /// Handle scanner failure states void handle_scanner_failure_(); /// Try to promote discovered clients to ready to connect From 078eaff9a8430fe717af4bf24ad46b2566e53929 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Aug 2025 06:49:47 +0200 Subject: [PATCH 05/21] [wifi] Fix reconnection failures after adapter restart by not clearing netif pointers (#10458) --- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 6 ------ esphome/components/wifi/wifi_component_esp_idf.cpp | 6 ------ 2 files changed, 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 67b1f565ff..89298e07c7 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -547,8 +547,6 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "STA stop"); - // Clear the STA interface handle to prevent use-after-free - s_sta_netif = nullptr; break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -638,10 +636,6 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { ESP_LOGV(TAG, "AP stop"); -#ifdef USE_WIFI_AP - // Clear the AP interface handle to prevent use-after-free - s_ap_netif = nullptr; -#endif break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 94f1f5125f..d465b346b3 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -697,8 +697,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); s_sta_started = false; - // Clear the STA interface handle to prevent use-after-free - s_sta_netif = nullptr; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; @@ -797,10 +795,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { ESP_LOGV(TAG, "AP stop"); s_ap_started = false; -#ifdef USE_WIFI_AP - // Clear the AP interface handle to prevent use-after-free - s_ap_netif = nullptr; -#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; From b6bb6699d1de62ce302a7b3931e52a39844b6cc7 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:30:01 +1000 Subject: [PATCH 06/21] [mipi_spi] Fix dimensions (#10443) --- esphome/components/mipi/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index f610f160b0..570a021cff 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -309,8 +309,12 @@ class DriverChip: CONF_NATIVE_HEIGHT, height + offset_height * 2 ) offset_height = native_height - height - offset_height - # Swap default dimensions if swap_xy is set - if transform[CONF_SWAP_XY] is True: + # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer + rotated = not requires_buffer(config) and config.get(CONF_ROTATION, 0) in ( + 90, + 270, + ) + if transform[CONF_SWAP_XY] is True or rotated: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height From 4ab37b069babaf53d6aaf06f23db118ce533fc01 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:30:33 +1000 Subject: [PATCH 07/21] [i2c] Perform register reads as single transactions (#10389) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../touchscreen/axs15231_touchscreen.cpp | 2 +- esphome/components/bmi160/bmi160.cpp | 2 +- .../components/bmp280_base/bmp280_base.cpp | 25 +- esphome/components/bmp280_base/bmp280_base.h | 10 +- esphome/components/bmp280_i2c/bmp280_i2c.cpp | 13 - esphome/components/bmp280_i2c/bmp280_i2c.h | 10 +- esphome/components/bmp280_spi/bmp280_spi.cpp | 8 +- esphome/components/bmp280_spi/bmp280_spi.h | 8 +- esphome/components/ch422g/ch422g.cpp | 4 +- esphome/components/ee895/ee895.cpp | 2 +- esphome/components/hte501/hte501.cpp | 5 +- esphome/components/i2c/__init__.py | 27 +- esphome/components/i2c/i2c.cpp | 64 ++-- esphome/components/i2c/i2c.h | 89 +++-- esphome/components/i2c/i2c_bus.h | 112 +++--- esphome/components/i2c/i2c_bus_arduino.cpp | 93 ++--- esphome/components/i2c/i2c_bus_arduino.h | 4 +- esphome/components/i2c/i2c_bus_esp_idf.cpp | 357 +++--------------- esphome/components/i2c/i2c_bus_esp_idf.h | 46 +-- esphome/components/iaqcore/iaqcore.cpp | 2 +- esphome/components/ina2xx_i2c/ina2xx_i2c.cpp | 2 +- esphome/components/kmeteriso/kmeteriso.cpp | 4 +- esphome/components/lc709203f/lc709203f.cpp | 4 +- esphome/components/mcp4461/mcp4461.cpp | 4 +- esphome/components/mlx90614/mlx90614.cpp | 6 +- esphome/components/mpl3115a2/mpl3115a2.cpp | 12 +- esphome/components/npi19/npi19.cpp | 2 +- esphome/components/opt3001/opt3001.cpp | 2 +- esphome/components/pca6416a/pca6416a.cpp | 6 +- esphome/components/pca9554/pca9554.cpp | 4 +- esphome/components/st7567_i2c/st7567_i2c.cpp | 3 +- esphome/components/tca9548a/tca9548a.cpp | 14 +- esphome/components/tca9548a/tca9548a.h | 4 +- esphome/components/tee501/tee501.cpp | 4 +- .../components/tlc59208f/tlc59208f_output.cpp | 3 +- esphome/components/veml3235/veml3235.cpp | 6 +- esphome/components/veml7700/veml7700.cpp | 8 +- esphome/components/veml7700/veml7700.h | 1 - 38 files changed, 329 insertions(+), 643 deletions(-) diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index 4adf0bbbe0..6304516164 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -41,7 +41,7 @@ void AXS15231Touchscreen::update_touches() { i2c::ErrorCode err; uint8_t data[8]{}; - err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false); + err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD)); ERROR_CHECK(err); err = this->read(data, sizeof(data)); ERROR_CHECK(err); diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index b041c7c2dc..4fcc3edb82 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -203,7 +203,7 @@ void BMI160Component::dump_config() { i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) { uint8_t raw_data[len * 2]; // read using read_register because we have little-endian data, and read_bytes_16 will swap it - i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true); + i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2); if (err != i2c::ERROR_OK) { return err; } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index e3cc2d9a57..39654f5875 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -63,12 +63,12 @@ void BMP280Component::setup() { // Read the chip id twice, to work around a bug where the first read is 0. // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 - if (!this->read_byte(0xD0, &chip_id)) { + if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; } - if (!this->read_byte(0xD0, &chip_id)) { + if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; @@ -80,7 +80,7 @@ void BMP280Component::setup() { } // Send a soft reset. - if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { + if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { this->mark_failed("Reset failed"); return; } @@ -89,7 +89,7 @@ void BMP280Component::setup() { uint8_t retry = 5; do { delay(2); - if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { + if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { this->mark_failed("Error reading status register"); return; } @@ -115,14 +115,14 @@ void BMP280Component::setup() { this->calibration_.p9 = this->read_s16_le_(0x9E); uint8_t config_register = 0; - if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { + if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { this->mark_failed("Read config"); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; - if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { + if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { this->mark_failed("Write config"); return; } @@ -159,7 +159,7 @@ void BMP280Component::update() { meas_value |= (this->temperature_oversampling_ & 0b111) << 5; meas_value |= (this->pressure_oversampling_ & 0b111) << 2; meas_value |= 0b01; // Forced mode - if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) { + if (!this->bmp_write_byte(BMP280_REGISTER_CONTROL, meas_value)) { this->status_set_warning(); return; } @@ -188,9 +188,10 @@ void BMP280Component::update() { } float BMP280Component::read_temperature_(int32_t *t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) + uint8_t data[3]{}; + if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) return NAN; + ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]); int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; if (adc == 0x80000) { @@ -212,7 +213,7 @@ float BMP280Component::read_temperature_(int32_t *t_fine) { float BMP280Component::read_pressure_(int32_t t_fine) { uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) + if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) return NAN; int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; @@ -258,12 +259,12 @@ void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } uint8_t BMP280Component::read_u8_(uint8_t a_register) { uint8_t data = 0; - this->read_byte(a_register, &data); + this->bmp_read_byte(a_register, &data); return data; } uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { uint16_t data = 0; - this->read_byte_16(a_register, &data); + this->bmp_read_byte_16(a_register, &data); return (data >> 8) | (data << 8); } int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index 4b22e98f13..a47a794e96 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -67,12 +67,12 @@ class BMP280Component : public PollingComponent { float get_setup_priority() const override; void update() override; - virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; - virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; - virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; - virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; - protected: + virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) = 0; + /// Read the temperature value and store the calculated ambient temperature in t_fine. float read_temperature_(int32_t *t_fine); /// Read the pressure value in hPa using the provided t_fine value. diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.cpp b/esphome/components/bmp280_i2c/bmp280_i2c.cpp index 04b8bd8b10..75d899008d 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.cpp +++ b/esphome/components/bmp280_i2c/bmp280_i2c.cpp @@ -5,19 +5,6 @@ namespace esphome { namespace bmp280_i2c { -bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { - return I2CDevice::read_byte(a_register, data); -}; -bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { - return I2CDevice::write_byte(a_register, data); -}; -bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { - return I2CDevice::read_bytes(a_register, data, len); -}; -bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { - return I2CDevice::read_byte_16(a_register, data); -}; - void BMP280I2CComponent::dump_config() { LOG_I2C_DEVICE(this); BMP280Component::dump_config(); diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.h b/esphome/components/bmp280_i2c/bmp280_i2c.h index 66d78d788b..0ac956202b 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.h +++ b/esphome/components/bmp280_i2c/bmp280_i2c.h @@ -11,10 +11,12 @@ static const char *const TAG = "bmp280_i2c.sensor"; /// This class implements support for the BMP280 Temperature+Pressure i2c sensor. class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice { public: - bool read_byte(uint8_t a_register, uint8_t *data) override; - bool write_byte(uint8_t a_register, uint8_t data) override; - bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; - bool read_byte_16(uint8_t a_register, uint16_t *data) override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override { return read_byte_16(a_register, data); } void dump_config() override; }; diff --git a/esphome/components/bmp280_spi/bmp280_spi.cpp b/esphome/components/bmp280_spi/bmp280_spi.cpp index a35e829432..88983e77c3 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.cpp +++ b/esphome/components/bmp280_spi/bmp280_spi.cpp @@ -28,7 +28,7 @@ void BMP280SPIComponent::setup() { // 0x77 is transferred, for read access, the byte 0xF7 is transferred. // https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf -bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { +bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); *data = this->transfer_byte(0); @@ -36,7 +36,7 @@ bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { return true; } -bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { +bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) { this->enable(); this->transfer_byte(clear_bit(a_register, 7)); this->transfer_byte(data); @@ -44,7 +44,7 @@ bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { return true; } -bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { +bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); this->read_array(data, len); @@ -52,7 +52,7 @@ bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t le return true; } -bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { +bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); ((uint8_t *) data)[1] = this->transfer_byte(0); diff --git a/esphome/components/bmp280_spi/bmp280_spi.h b/esphome/components/bmp280_spi/bmp280_spi.h index dd226502f6..1bb7678e55 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.h +++ b/esphome/components/bmp280_spi/bmp280_spi.h @@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, public spi::SPIDevice { void setup() override; - bool read_byte(uint8_t a_register, uint8_t *data) override; - bool write_byte(uint8_t a_register, uint8_t data) override; - bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; - bool read_byte_16(uint8_t a_register, uint16_t *data) override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override; + bool bmp_write_byte(uint8_t a_register, uint8_t data) override; + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override; }; } // namespace bmp280_spi diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 6f652cb0c6..9a4e342525 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -91,7 +91,7 @@ bool CH422GComponent::read_inputs_() { // Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { - auto err = this->bus_->write(reg, &value, 1); + auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0); if (err != i2c::ERROR_OK) { this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str()); return false; @@ -102,7 +102,7 @@ bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { uint8_t CH422GComponent::read_reg_(uint8_t reg) { uint8_t value; - auto err = this->bus_->read(reg, &value, 1); + auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1); if (err != i2c::ERROR_OK) { this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str()); return 0; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 3a8a9b3725..c6eaf4e728 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -83,7 +83,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { crc16 = calc_crc16_(address, 6); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; - this->write(address, 7, true); + this->write(address, 7); } float EE895Component::read_float_() { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 75770ceffe..911cafe97a 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -9,9 +9,8 @@ static const char *const TAG = "hte501"; void HTE501Component::setup() { uint8_t address[] = {0x70, 0x29}; - this->write(address, 2, false); uint8_t identification[9]; - this->read(identification, 9); + this->write_read(address, sizeof address, identification, sizeof identification); if (identification[8] != calc_crc8_(identification, 0, 7)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); @@ -42,7 +41,7 @@ void HTE501Component::dump_config() { float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; - this->write(address_1, 2, true); + this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 4172b23845..35b9fab9e4 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,7 +2,6 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32 from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -14,8 +13,6 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, - KEY_CORE, - KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -48,28 +45,8 @@ def _bus_declare_type(value): def validate_config(config): - if ( - config[CONF_SCAN] - and CORE.is_esp32 - and CORE.using_esp_idf - and esp32.get_esp32_variant() - in [ - esp32.const.VARIANT_ESP32C5, - esp32.const.VARIANT_ESP32C6, - esp32.const.VARIANT_ESP32P4, - ] - ): - version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if version.major == 5 and ( - (version.minor == 3 and version.patch <= 3) - or (version.minor == 4 and version.patch <= 1) - ): - LOGGER.warning( - "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " - "has been disabled, see https://github.com/esphome/issues/issues/7128", - str(version), - ) - config[CONF_SCAN] = False + if CORE.using_esp_idf: + return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config) return config diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 2b2190d28b..e66ab8ba73 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,4 +1,6 @@ #include "i2c.h" + +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include @@ -7,38 +9,48 @@ namespace i2c { static const char *const TAG = "i2c"; -ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - ErrorCode err = this->write(&a_register, 1, stop); - if (err != ERROR_OK) - return err; - return bus_->read(address_, data, len); +void I2CBus::i2c_scan_() { + // suppress logs from the IDF I2C library during the scan +#if defined(USE_ESP32) && defined(USE_LOGGER) + auto previous = esp_log_level_get("*"); + esp_log_level_set("*", ESP_LOG_NONE); +#endif + + for (uint8_t address = 8; address != 120; address++) { + auto err = write_readv(address, nullptr, 0, nullptr, 0); + if (err == ERROR_OK) { + scan_results_.emplace_back(address, true); + } else if (err == ERROR_UNKNOWN) { + scan_results_.emplace_back(address, false); + } + } +#if defined(USE_ESP32) && defined(USE_LOGGER) + esp_log_level_set("*", previous); +#endif } -ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { +ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) { + return bus_->write_readv(this->address_, &a_register, 1, data, len); +} + +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len) { a_register = convert_big_endian(a_register); - ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); - if (err != ERROR_OK) - return err; - return bus_->read(address_, data, len); + return bus_->write_readv(this->address_, reinterpret_cast(&a_register), 2, data, len); } -ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { - WriteBuffer buffers[2]; - buffers[0].data = &a_register; - buffers[0].len = 1; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); +ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { + std::vector v{}; + v.push_back(a_register); + v.insert(v.end(), data, data + len); + return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); } -ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { - a_register = convert_big_endian(a_register); - WriteBuffer buffers[2]; - buffers[0].data = reinterpret_cast(&a_register); - buffers[0].len = 2; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { + std::vector v(len + 2); + v.push_back(a_register >> 8); + v.push_back(a_register); + v.insert(v.end(), data, data + len); + return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { @@ -49,7 +61,7 @@ bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { return true; } -bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { +bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; for (size_t i = 0; i < len; i++) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 15f786245b..48a6e751cf 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,10 +1,10 @@ #pragma once -#include "i2c_bus.h" -#include "esphome/core/helpers.h" -#include "esphome/core/optional.h" #include #include +#include "esphome/core/helpers.h" +#include "esphome/core/optional.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -161,51 +161,53 @@ class I2CDevice { /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read /// @return an i2c::ErrorCode - ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + ErrorCode read(uint8_t *data, size_t len) const { return bus_->write_readv(this->address_, nullptr, 0, data, len); } /// @brief reads an array of bytes from a specific register in the I²C device /// @param a_register an 8 bits internal address of the I²C register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len); /// @brief reads an array of bytes from a specific register in the I²C device /// @param a_register the 16 bits internal address of the I²C register to read from /// @param data pointer to an array of bytes to store the information /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len); /// @brief writes an array of bytes to a device using an I2CBus /// @param data pointer to an array that contains the bytes to send /// @param len length of the buffer = number of bytes to write - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + ErrorCode write(const uint8_t *data, size_t len) const { + return bus_->write_readv(this->address_, data, len, nullptr, 0); + } + + /// @brief writes an array of bytes to a device, then reads an array, as a single transaction + /// @param write_data pointer to an array that contains the bytes to send + /// @param write_len length of the buffer = number of bytes to write + /// @param read_data pointer to an array to store the bytes read + /// @param read_len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode + ErrorCode write_read(const uint8_t *write_data, size_t write_len, uint8_t *read_data, size_t read_len) const { + return bus_->write_readv(this->address_, write_data, write_len, read_data, read_len); + } /// @brief writes an array of bytes to a specific register in the I²C device /// @param a_register the internal address of the register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) const; /// @brief write an array of bytes to a specific register in the I²C device /// @param a_register the 16 bits internal address of the register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len) const; /// /// Compat APIs @@ -217,7 +219,7 @@ class I2CDevice { return read_register(a_register, data, len) == ERROR_OK; } - bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } + bool read_bytes_raw(uint8_t *data, uint8_t len) const { return read(data, len) == ERROR_OK; } template optional> read_bytes(uint8_t a_register) { std::array res; @@ -236,9 +238,7 @@ class I2CDevice { bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); - bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { - return read_register(a_register, data, 1, stop) == ERROR_OK; - } + bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } optional read_byte(uint8_t a_register) { uint8_t data; @@ -249,11 +249,11 @@ class I2CDevice { bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) { - return write_register(a_register, data, len, stop) == ERROR_OK; + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) const { + return write_register(a_register, data, len) == ERROR_OK; } - bool write_bytes(uint8_t a_register, const std::vector &data) { + bool write_bytes(uint8_t a_register, const std::vector &data) const { return write_bytes(a_register, data.data(), data.size()); } @@ -261,13 +261,42 @@ class I2CDevice { return write_bytes(a_register, data.data(), data.size()); } - bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); + bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const; - bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) { - return write_bytes(a_register, &data, 1, stop); + bool write_byte(uint8_t a_register, uint8_t data) const { return write_bytes(a_register, &data, 1); } + + bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } + + // Deprecated functions + + ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { + return this->read_register(a_register, data, len); } - bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } + ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + return this->read_register16(a_register, data, len); + } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { + return this->write_register(a_register, data, len); + } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { + return this->write_register16(a_register, data, len); + } protected: uint8_t address_{0x00}; ///< store the address of the device on the bus diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index da94aa940d..df4df628e8 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,9 +1,12 @@ #pragma once #include #include +#include #include #include +#include "esphome/core/helpers.h" + namespace esphome { namespace i2c { @@ -39,71 +42,66 @@ struct WriteBuffer { /// note https://www.nxp.com/docs/en/application-note/AN10216.pdf class I2CBus { public: - /// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer - /// @param address address of the I²C component on the i2c bus - /// @param buffer pointer to an array of bytes that will be used to store the data received - /// @param len length of the buffer = number of bytes to read - /// @return an i2c::ErrorCode - virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { - ReadBuffer buf; - buf.data = buffer; - buf.len = len; - return readv(address, &buf, 1); - } + virtual ~I2CBus() = default; - /// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer. - /// @param address address of the I²C component on the i2c bus - /// @param buffers pointer to an array of ReadBuffer - /// @param count number of ReadBuffer to read - /// @return an i2c::ErrorCode - /// @details This is a pure virtual method that must be implemented in a subclass. - virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0; - - virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { - return write(address, buffer, len, true); - } - - /// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer - /// @param address address of the I²C component on the i2c bus - /// @param buffer pointer to an array of bytes that contains the data to be sent - /// @param len length of the buffer = number of bytes to write - /// @param stop true or false: True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. - /// @return an i2c::ErrorCode - virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { - WriteBuffer buf; - buf.data = buffer; - buf.len = len; - return writev(address, &buf, 1, stop); - } - - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { - return writev(address, buffers, cnt, true); - } - - /// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer. - /// @param address address of the I²C component on the i2c bus - /// @param buffers pointer to an array of WriteBuffer - /// @param count number of WriteBuffer to write - /// @param stop true or false: True will send a stop message, releasing the bus after + /// @brief This virtual method writes bytes to an I2CBus from an array, + /// then reads bytes into an array of ReadBuffer. + /// @param address address of the I²C device on the i2c bus + /// @param write_buffer pointer to data + /// @param write_count number of bytes to write + /// @param read_buffer pointer to an array to receive data + /// @param read_count number of bytes to read /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode /// @details This is a pure virtual method that must be implemented in the subclass. - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0; + virtual ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) = 0; + + // Legacy functions for compatibility + + ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { + return this->write_readv(address, nullptr, 0, buffer, len); + } + + ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop = true) { + return this->write_readv(address, buffer, len, nullptr, 0); + } + + ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", + "2025.9.0") + ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { + size_t total_len = 0; + for (size_t i = 0; i != count; i++) { + total_len += read_buffers[i].len; + } + std::vector buffer(total_len); + auto err = this->write_readv(address, nullptr, 0, buffer.data(), total_len); + if (err != ERROR_OK) + return err; + size_t pos = 0; + for (size_t i = 0; i != count; i++) { + if (read_buffers[i].len != 0) { + std::memcpy(read_buffers[i].data, buffer.data() + pos, read_buffers[i].len); + pos += read_buffers[i].len; + } + } + return ERROR_OK; + } + + ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", + "2025.9.0") + ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { + std::vector buffer{}; + for (size_t i = 0; i != count; i++) { + buffer.insert(buffer.end(), write_buffers[i].data, write_buffers[i].data + write_buffers[i].len); + } + return this->write_readv(address, buffer.data(), buffer.size(), nullptr, 0); + } protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. - virtual void i2c_scan() { - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - if (err == ERROR_OK) { - scan_results_.emplace_back(address, true); - } else if (err == ERROR_UNKNOWN) { - scan_results_.emplace_back(address, false); - } - } - } + void i2c_scan_(); std::vector> scan_results_; ///< array containing scan results bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 24385745eb..221423418b 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -41,7 +41,7 @@ void ArduinoI2CBus::setup() { this->initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan(); + this->i2c_scan_(); } } @@ -111,88 +111,37 @@ void ArduinoI2CBus::dump_config() { } } -ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { +ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { #if defined(USE_ESP8266) this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances #endif - - // logging is only enabled with vv level, if warnings are shown the caller - // should log them if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); - return ERROR_NOT_INITIALIZED; - } - size_t to_request = 0; - for (size_t i = 0; i < cnt; i++) - to_request += buffers[i].len; - size_t ret = wire_->requestFrom(address, to_request, true); - if (ret != to_request) { - ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret); - return ERROR_TIMEOUT; - } - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) - buf.data[j] = wire_->read(); - } - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); -#endif - - return ERROR_OK; -} -ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { -#if defined(USE_ESP8266) - this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances -#endif - - // logging is only enabled with vv level, if warnings are shown the caller - // should log them - if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); + ESP_LOGD(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); -#endif - - wire_->beginTransmission(address); - size_t written = 0; - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - size_t ret = wire_->write(buf.data, buf.len); - written += ret; - if (ret != buf.len) { - ESP_LOGVV(TAG, "TX failed at %u", written); + uint8_t status = 0; + if (write_count != 0 || read_count == 0) { + wire_->beginTransmission(address); + size_t ret = wire_->write(write_buffer, write_count); + if (ret != write_count) { + ESP_LOGV(TAG, "TX failed"); return ERROR_UNKNOWN; } + status = wire_->endTransmission(read_count == 0); + } + if (status == 0 && read_count != 0) { + size_t ret2 = wire_->requestFrom(address, read_count, true); + if (ret2 != read_count) { + ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2); + return ERROR_TIMEOUT; + } + for (size_t j = 0; j != read_count; j++) + read_buffer[j] = wire_->read(); } - uint8_t status = wire_->endTransmission(stop); switch (status) { case 0: return ERROR_OK; diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 7e6616cbce..b441828353 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -19,8 +19,8 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; - ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index cf31ba1c0d..bf50ea0586 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP_IDF #include "i2c_bus_esp_idf.h" + #include #include #include @@ -9,10 +10,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#define SOC_HP_I2C_NUM SOC_I2C_NUM -#endif - namespace esphome { namespace i2c { @@ -34,7 +31,6 @@ void IDFI2CBus::setup() { this->recover_(); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) next_port = (i2c_port_t) (next_port + 1); i2c_master_bus_config_t bus_conf{}; @@ -77,56 +73,8 @@ void IDFI2CBus::setup() { if (this->scan_) { ESP_LOGV(TAG, "Scanning for devices"); - this->i2c_scan(); + this->i2c_scan_(); } -#else -#if SOC_HP_I2C_NUM > 1 - next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; -#else - next_port = I2C_NUM_MAX; -#endif - - i2c_config_t conf{}; - memset(&conf, 0, sizeof(conf)); - conf.mode = I2C_MODE_MASTER; - conf.sda_io_num = sda_pin_; - conf.sda_pullup_en = sda_pullup_enabled_; - conf.scl_io_num = scl_pin_; - conf.scl_pullup_en = scl_pullup_enabled_; - conf.master.clk_speed = frequency_; -#ifdef USE_ESP32_VARIANT_ESP32S2 - // workaround for https://github.com/esphome/issues/issues/6718 - conf.clk_flags = I2C_SCLK_SRC_FLAG_AWARE_DFS; -#endif - esp_err_t err = i2c_param_config(port_, &conf); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - if (timeout_ > 0) { - err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } else { - ESP_LOGV(TAG, "i2c_timeout set to %" PRIu32 " ticks (%" PRIu32 " us)", timeout_ * 80, timeout_); - } - } - err = i2c_driver_install(port_, I2C_MODE_MASTER, 0, 0, 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - initialized_ = true; - if (this->scan_) { - ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan(); - } -#endif } void IDFI2CBus::dump_config() { @@ -166,267 +114,73 @@ void IDFI2CBus::dump_config() { } } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) -void IDFI2CBus::i2c_scan() { - for (uint8_t address = 8; address < 120; address++) { - auto err = i2c_master_probe(this->bus_, address, 20); - if (err == ESP_OK) { - this->scan_results_.emplace_back(address, true); - } - } -} -#endif - -ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { - // logging is only enabled with vv level, if warnings are shown the caller +ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) { + // logging is only enabled with v level, if warnings are shown the caller // should log them if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); + ESP_LOGW(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_operation_job_t jobs[cnt + 4]; - uint8_t read = (address << 1) | I2C_MASTER_READ; - size_t last = 0, num = 0; - - jobs[num].command = I2C_MASTER_CMD_START; - num++; - - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = &read; - jobs[num].write.total_bytes = 1; - num++; - - // find the last valid index - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; + i2c_operation_job_t jobs[8]{}; + size_t num_jobs = 0; + uint8_t write_addr = (address << 1) | I2C_MASTER_WRITE; + uint8_t read_addr = (address << 1) | I2C_MASTER_READ; + ESP_LOGV(TAG, "Writing %zu bytes, reading %zu bytes", write_count, read_count); + if (read_count == 0 && write_count == 0) { + // basically just a bus probe. Send a start, address and stop + ESP_LOGV(TAG, "0x%02X BUS PROBE", address); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &write_addr; + jobs[num_jobs++].write.total_bytes = 1; + } else { + if (write_count != 0) { + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &write_addr; + jobs[num_jobs++].write.total_bytes = 1; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = (uint8_t *) write_buffer; + jobs[num_jobs++].write.total_bytes = write_count; } - last = i; - } - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; - } - if (i == last) { - // the last byte read before stop should always be a nack, - // split the last read if len is larger than 1 - if (buf.len > 1) { - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_ACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data; - jobs[num].read.total_bytes = buf.len - 1; - num++; + if (read_count != 0) { + ESP_LOGV(TAG, "0x%02X RX bytes %zu", address, read_count); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &read_addr; + jobs[num_jobs++].write.total_bytes = 1; + if (read_count > 1) { + jobs[num_jobs].command = I2C_MASTER_CMD_READ; + jobs[num_jobs].read.ack_value = I2C_ACK_VAL; + jobs[num_jobs].read.data = read_buffer; + jobs[num_jobs++].read.total_bytes = read_count - 1; } - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_NACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1; - jobs[num].read.total_bytes = 1; - num++; - } else { - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_ACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data; - jobs[num].read.total_bytes = buf.len; - num++; + jobs[num_jobs].command = I2C_MASTER_CMD_READ; + jobs[num_jobs].read.ack_value = I2C_NACK_VAL; + jobs[num_jobs].read.data = read_buffer + read_count - 1; + jobs[num_jobs++].read.total_bytes = 1; } } - - jobs[num].command = I2C_MASTER_CMD_STOP; - num++; - - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); + jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; + ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); + esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20); if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); + ESP_LOGV(TAG, "TX to %02X failed: not acked", address); return ERROR_NOT_ACKNOWLEDGED; } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); + ESP_LOGV(TAG, "TX to %02X failed: timeout", address); return ERROR_TIMEOUT; } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); + ESP_LOGV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); return ERROR_UNKNOWN; } -#else - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - esp_err_t err = i2c_master_start(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X master start failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X address write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - err = i2c_master_read(cmd, buf.data, buf.len, i == cnt - 1 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X data read failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); - // i2c_master_cmd_begin() will block for a whole second if no ack: - // https://github.com/espressif/esp-idf/issues/4999 - i2c_cmd_link_delete(cmd); - if (err == ESP_FAIL) { - // transfer not acked - ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#endif - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); -#endif - - return ERROR_OK; -} - -ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { - // logging is only enabled with vv level, if warnings are shown the caller - // should log them - if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); - return ERROR_NOT_INITIALIZED; - } - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); -#endif - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_operation_job_t jobs[cnt + 3]; - uint8_t write = (address << 1) | I2C_MASTER_WRITE; - size_t num = 0; - - jobs[num].command = I2C_MASTER_CMD_START; - num++; - - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = &write; - jobs[num].write.total_bytes = 1; - num++; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; - } - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = (uint8_t *) buf.data; - jobs[num].write.total_bytes = buf.len; - num++; - } - - if (stop) { - jobs[num].command = I2C_MASTER_CMD_STOP; - num++; - } - - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); - if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#else - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - esp_err_t err = i2c_master_start(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master start failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X address write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - err = i2c_master_write(cmd, buf.data, buf.len, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X data write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - if (stop) { - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); - i2c_cmd_link_delete(cmd); - if (err == ESP_FAIL) { - // transfer not acked - ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#endif return ERROR_OK; } @@ -436,8 +190,8 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b void IDFI2CBus::recover_() { ESP_LOGI(TAG, "Performing bus recovery"); - const gpio_num_t scl_pin = static_cast(scl_pin_); - const gpio_num_t sda_pin = static_cast(sda_pin_); + const auto scl_pin = static_cast(scl_pin_); + const auto sda_pin = static_cast(sda_pin_); // For the upcoming operations, target for a 60kHz toggle frequency. // 1000kHz is the maximum frequency for I2C running in standard-mode, @@ -545,5 +299,4 @@ void IDFI2CBus::recover_() { } // namespace i2c } // namespace esphome - #endif // USE_ESP_IDF diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 4e8f86fd0c..f565be4535 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,14 +2,9 @@ #ifdef USE_ESP_IDF -#include "esp_idf_version.h" #include "esphome/core/component.h" #include "i2c_bus.h" -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #include -#else -#include -#endif namespace esphome { namespace i2c { @@ -24,36 +19,33 @@ class IDFI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; - ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } - void set_scan(bool scan) { scan_ = scan; } - void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; } - void set_sda_pullup_enabled(bool sda_pullup_enabled) { sda_pullup_enabled_ = sda_pullup_enabled; } - void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; } - void set_scl_pullup_enabled(bool scl_pullup_enabled) { scl_pullup_enabled_ = scl_pullup_enabled; } - void set_frequency(uint32_t frequency) { frequency_ = frequency; } - void set_timeout(uint32_t timeout) { timeout_ = timeout; } + void set_scan(bool scan) { this->scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_sda_pullup_enabled(bool sda_pullup_enabled) { this->sda_pullup_enabled_ = sda_pullup_enabled; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_scl_pullup_enabled(bool scl_pullup_enabled) { this->scl_pullup_enabled_ = scl_pullup_enabled; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } - int get_port() const override { return static_cast(this->port_); } + int get_port() const override { return this->port_; } private: void recover_(); - RecoveryCode recovery_result_; + RecoveryCode recovery_result_{}; protected: -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_master_dev_handle_t dev_; - i2c_master_bus_handle_t bus_; - void i2c_scan() override; -#endif - i2c_port_t port_; - uint8_t sda_pin_; - bool sda_pullup_enabled_; - uint8_t scl_pin_; - bool scl_pullup_enabled_; - uint32_t frequency_; + i2c_master_dev_handle_t dev_{}; + i2c_master_bus_handle_t bus_{}; + i2c_port_t port_{}; + uint8_t sda_pin_{}; + bool sda_pullup_enabled_{}; + uint8_t scl_pin_{}; + bool scl_pullup_enabled_{}; + uint32_t frequency_{}; uint32_t timeout_ = 0; bool initialized_ = false; }; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 2a84eabf75..274f9086b6 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -35,7 +35,7 @@ void IAQCore::setup() { void IAQCore::update() { uint8_t buffer[sizeof(SensorData)]; - if (this->read_register(0xB5, buffer, sizeof(buffer), false) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp index d28525635d..a363a9c12f 100644 --- a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp @@ -21,7 +21,7 @@ void INA2XXI2C::dump_config() { } bool INA2XXI2C::read_ina_register(uint8_t reg, uint8_t *data, size_t len) { - auto ret = this->read_register(reg, data, len, false); + auto ret = this->read_register(reg, data, len); if (ret != i2c::ERROR_OK) { ESP_LOGE(TAG, "read_ina_register_ failed. Reg=0x%02X Err=%d", reg, ret); } diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 3aedac3f5f..d20e07460b 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -22,7 +22,7 @@ void KMeterISOComponent::setup() { this->reset_to_construction_state(); } - auto err = this->bus_->writev(this->address_, nullptr, 0); + auto err = this->bus_->write_readv(this->address_, nullptr, 0, nullptr, 0); if (err == esphome::i2c::ERROR_OK) { ESP_LOGCONFIG(TAG, "Could write to the address %d.", this->address_); } else { @@ -33,7 +33,7 @@ void KMeterISOComponent::setup() { } uint8_t read_buf[4] = {1}; - if (!this->read_bytes(KMETER_ERROR_STATUS_REG, read_buf, 1)) { + if (!this->read_register(KMETER_ERROR_STATUS_REG, read_buf, 1)) { ESP_LOGCONFIG(TAG, "Could not read from the device."); this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index e5d12a75d4..d41c1f6bc7 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -184,7 +184,7 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va // function will send a stop between the read and the write portion of the I2C // transaction. This is bad in this case and will result in reading nothing but 0xFFFF // from the registers. - return_code = this->read_register(register_to_read, &read_buffer[3], 3, false); + return_code = this->read_register(register_to_read, &read_buffer[3], 3); if (return_code != i2c::NO_ERROR) { // Error on the i2c bus this->status_set_warning( @@ -225,7 +225,7 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) { // Note: we don't write the first byte of the write buffer to the device. // This is done automatically by the write() function. - return_code = this->write(&write_buffer[1], 4, true); + return_code = this->write(&write_buffer[1], 4); if (return_code == i2c::NO_ERROR) { return return_code; } else { diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 6634c5057e..55ce9b7899 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -328,7 +328,7 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); - auto err = this->write(&this->address_, reg, sizeof(reg)); + auto err = this->write(&this->address_, reg); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); @@ -359,7 +359,7 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::DECREMENT); - auto err = this->write(&this->address_, reg, sizeof(reg)); + auto err = this->write(&this->address_, reg); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 2e711baf9a..1341e6935b 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -90,18 +90,18 @@ float MLX90614Component::get_setup_priority() const { return setup_priority::DAT void MLX90614Component::update() { uint8_t emissivity[3]; - if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } uint8_t raw_object[3]; - if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } uint8_t raw_ambient[3]; - if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } diff --git a/esphome/components/mpl3115a2/mpl3115a2.cpp b/esphome/components/mpl3115a2/mpl3115a2.cpp index 9e8467a29b..a689149c89 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.cpp +++ b/esphome/components/mpl3115a2/mpl3115a2.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "mpl3115a2"; void MPL3115A2Component::setup() { uint8_t whoami = 0xFF; - if (!this->read_byte(MPL3115A2_WHOAMI, &whoami, false)) { + if (!this->read_byte(MPL3115A2_WHOAMI, &whoami)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; @@ -54,24 +54,24 @@ void MPL3115A2Component::dump_config() { void MPL3115A2Component::update() { uint8_t mode = MPL3115A2_CTRL_REG1_OS128; - this->write_byte(MPL3115A2_CTRL_REG1, mode, true); + this->write_byte(MPL3115A2_CTRL_REG1, mode); // Trigger a new reading mode |= MPL3115A2_CTRL_REG1_OST; if (this->altitude_ != nullptr) mode |= MPL3115A2_CTRL_REG1_ALT; - this->write_byte(MPL3115A2_CTRL_REG1, mode, true); + this->write_byte(MPL3115A2_CTRL_REG1, mode); // Wait until status shows reading available uint8_t status = 0; - if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status, false) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { + if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { delay(10); - if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status, false) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { + if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { return; } } uint8_t buffer[5] = {0, 0, 0, 0, 0}; - this->read_register(MPL3115A2_REGISTER_PRESSURE_MSB, buffer, 5, false); + this->read_register(MPL3115A2_REGISTER_PRESSURE_MSB, buffer, 5); float altitude = 0, pressure = 0; if (this->altitude_ != nullptr) { diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index e8c4e8abd5..c531d2ec8f 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -33,7 +33,7 @@ float NPI19Component::get_setup_priority() const { return setup_priority::DATA; i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { // initiate data read from device - i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND), true); + i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND)); if (w_err != i2c::ERROR_OK) { return w_err; } diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index 2d65f1090d..f5f7ab9412 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -72,7 +72,7 @@ void OPT3001Sensor::read_lx_(const std::function &f) { } this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { - if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { + if (this->write(&OPT3001_REG_CONFIGURATION, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Starting configuration register read failed"); f(NAN); return; diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index dc8662d1a2..730c494e34 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -33,7 +33,7 @@ void PCA6416AComponent::setup() { } // Test to see if the device supports pull-up resistors - if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == i2c::ERROR_OK) { + if (this->read_register(PCAL6416A_PULL_EN0, &value, 1) == i2c::ERROR_OK) { this->has_pullup_ = true; } @@ -105,7 +105,7 @@ bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) { return false; } - this->last_error_ = this->read_register(reg, value, 1, true); + this->last_error_ = this->read_register(reg, value, 1); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -122,7 +122,7 @@ bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) { return false; } - this->last_error_ = this->write_register(reg, &value, 1, true); + this->last_error_ = this->write_register(reg, &value, 1); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index f77d680bec..1166cc1a09 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -96,7 +96,7 @@ bool PCA9554Component::read_inputs_() { return false; } - this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true); + this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -114,7 +114,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { uint8_t outputs[2]; outputs[0] = (uint8_t) value; outputs[1] = (uint8_t) (value >> 8); - this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true); + this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 4970367343..710e473b11 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -51,8 +51,7 @@ void HOT I2CST7567::write_display_data() { static const size_t BLOCK_SIZE = 64; for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], - this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x, - true); + this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x); } } } diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index edd8af9a27..1de3c49108 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -6,23 +6,15 @@ namespace tca9548a { static const char *const TAG = "tca9548a"; -i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) { +i2c::ErrorCode TCA9548AChannel::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - err = this->parent_->bus_->readv(address, buffers, cnt); + err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count); this->parent_->disable_all_channels(); return err; } -i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { - auto err = this->parent_->switch_to_channel(channel_); - if (err != i2c::ERROR_OK) - return err; - err = this->parent_->bus_->writev(address, buffers, cnt, stop); - this->parent_->disable_all_channels(); - return err; -} - void TCA9548AComponent::setup() { uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 08f1674d11..0fb9ada99a 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -14,8 +14,8 @@ class TCA9548AChannel : public i2c::I2CBus { void set_channel(uint8_t channel) { channel_ = channel; } void set_parent(TCA9548AComponent *parent) { parent_ = parent; } - i2c::ErrorCode readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) override; - i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) override; + i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; protected: uint8_t channel_; diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 460f446865..4f8003eb10 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -9,9 +9,9 @@ static const char *const TAG = "tee501"; void TEE501Component::setup() { uint8_t address[] = {0x70, 0x29}; - this->write(address, 2, false); uint8_t identification[9]; this->read(identification, 9); + this->write_read(address, sizeof address, identification, sizeof identification); if (identification[8] != calc_crc8_(identification, 0, 7)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); @@ -41,7 +41,7 @@ void TEE501Component::dump_config() { float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } void TEE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; - this->write(address_1, 2, true); + this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[3]; this->read(i2c_response, 3); diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index a524f92f75..85311a877c 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -74,7 +74,8 @@ void TLC59208FOutput::setup() { ESP_LOGV(TAG, " Resetting all devices on the bus"); // Reset all devices on the bus - if (this->bus_->write(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, 2) != i2c::ERROR_OK) { + if (this->bus_->write_readv(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, sizeof TLC59208F_SWRST_SEQ, nullptr, 0) != + i2c::ERROR_OK) { ESP_LOGE(TAG, "RESET failed"); this->mark_failed(); return; diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index f3016fb171..1e02e3e802 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -14,14 +14,12 @@ void VEML3235Sensor::setup() { this->mark_failed(); return; } - if ((this->write(&ID_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(device_id, 2)) { + if ((this->read_register(ID_REG, device_id, sizeof device_id) != i2c::ERROR_OK)) { ESP_LOGE(TAG, "Unable to read ID"); this->mark_failed(); - return; } else if (device_id[0] != DEVICE_ID) { ESP_LOGE(TAG, "Incorrect device ID - expected 0x%.2x, read 0x%.2x", DEVICE_ID, device_id[0]); this->mark_failed(); - return; } } @@ -49,7 +47,7 @@ float VEML3235Sensor::read_lx_() { } uint8_t als_regs[] = {0, 0}; - if ((this->write(&ALS_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(als_regs, 2)) { + if ((this->read_register(ALS_REG, als_regs, sizeof als_regs) != i2c::ERROR_OK)) { this->status_set_warning(); return NAN; } diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 2a4c246ac9..c3b601e288 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -279,20 +279,18 @@ ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Ga } ErrorCode VEML7700Component::read_sensor_output_(Readings &data) { - auto als_err = - this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE, false); + auto als_err = this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE); if (als_err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading ALS register, err = %d", als_err); } auto white_err = - this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE, false); + this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE); if (white_err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading WHITE register, err = %d", white_err); } ConfigurationRegister conf{0}; - auto err = - this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE, false); + auto err = this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE); if (err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading ALS_CONF_0 register, err = %d", white_err); } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index b0d1451cf0..4b5edf733d 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -3,7 +3,6 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -#include "esphome/core/optional.h" namespace esphome { namespace veml7700 { From a11970aee0360d53f256330d4f5c19b437995546 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 28 Aug 2025 07:16:26 +1000 Subject: [PATCH 08/21] [wifi] Fix retry with hidden networks. (#10445) --- esphome/components/wifi/wifi_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 987e276e0c..d16c94fa13 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -151,6 +151,8 @@ void WiFiComponent::loop() { this->status_set_warning("waiting to reconnect"); if (millis() - this->action_started_ > 5000) { if (this->fast_connect_ || this->retry_hidden_) { + if (!this->selected_ap_.get_bssid().has_value()) + this->selected_ap_ = this->sta_[0]; this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); @@ -670,10 +672,12 @@ void WiFiComponent::check_connecting_finished() { return; } + ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again + if (this->retry_hidden_ && !this->selected_ap_.get_hidden()) + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", this->selected_ap_.get_ssid().c_str()); this->retry_hidden_ = false; - ESP_LOGI(TAG, "Connected"); this->print_connect_params_(); if (this->has_ap()) { From e513c0f0043f6814659e6c871d2b6fc4a32e2e48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Aug 2025 23:23:43 +0200 Subject: [PATCH 09/21] Fix AttributeError when uploading OTA to offline OpenThread devices (#10459) --- esphome/__main__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 8e8fc7d5d9..aab3035a5e 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -132,14 +132,17 @@ def choose_upload_log_host( ] resolved.append(choose_prompt(options, purpose=purpose)) elif device == "OTA": - if (show_ota and "ota" in CORE.config) or ( - show_api and "api" in CORE.config + if CORE.address and ( + (show_ota and "ota" in CORE.config) + or (show_api and "api" in CORE.config) ): resolved.append(CORE.address) elif show_mqtt and has_mqtt_logging(): resolved.append("MQTT") else: resolved.append(device) + if not resolved: + _LOGGER.error("All specified devices: %s could not be resolved.", defaults) return resolved # No devices specified, show interactive chooser From 015977cfdfcf99e781af4d31e964c67d508dbc3a Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 27 Aug 2025 21:53:57 -0400 Subject: [PATCH 10/21] [rtttl] Fix RTTTL for speakers (#10381) --- esphome/components/rtttl/rtttl.cpp | 63 +++++++++++++++++------------- esphome/components/rtttl/rtttl.h | 25 ++++++++++++ 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65a3af1bbc..5aedc74489 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -138,11 +138,37 @@ void Rtttl::stop() { this->set_state_(STATE_STOPPING); } #endif + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} + +void Rtttl::finish_() { + ESP_LOGV(TAG, "Rtttl::finish_()"); +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); + } +#endif + // Ensure no more notes are played in case finish_() is called for an error. + this->position_ = this->rtttl_.length(); this->note_duration_ = 0; } void Rtttl::loop() { - if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { + if (this->state_ == State::STATE_STOPPED) { this->disable_loop(); return; } @@ -152,6 +178,8 @@ void Rtttl::loop() { if (this->state_ == State::STATE_STOPPING) { if (this->speaker_->is_stopped()) { this->set_state_(State::STATE_STOPPED); + } else { + return; } } else if (this->state_ == State::STATE_INIT) { if (this->speaker_->is_stopped()) { @@ -207,7 +235,7 @@ void Rtttl::loop() { if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) return; #endif - if (!this->rtttl_[this->position_]) { + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } @@ -346,31 +374,6 @@ void Rtttl::loop() { this->last_note_ = millis(); } -void Rtttl::finish_() { -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(State::STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); - - this->speaker_->finish(); - this->set_state_(State::STATE_STOPPING); - } -#endif - this->note_duration_ = 0; - this->on_finished_playback_callback_.call(); - ESP_LOGD(TAG, "Playback finished"); -} - #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *state_to_string(State state) { switch (state) { @@ -397,7 +400,11 @@ void Rtttl::set_state_(State state) { LOG_STR_ARG(state_to_string(state))); // Clear loop_done when transitioning from STOPPED to any other state - if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { + if (state == State::STATE_STOPPED) { + this->disable_loop(); + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); + } else if (old_state == State::STATE_STOPPED) { this->enable_loop(); } } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 420948bfbf..d536c6c08e 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -60,35 +60,60 @@ class Rtttl : public Component { } return ret; } + /** + * @brief Finalizes the playback of the RTTTL string. + * + * This method is called internally when the end of the RTTTL string is reached + * or when a parsing error occurs. It stops the output, sets the component state, + * and triggers the on_finished_playback_callback_. + */ void finish_(); void set_state_(State state); + /// The RTTTL string to play. std::string rtttl_{""}; + /// The current position in the RTTTL string. size_t position_{0}; + /// The duration of a whole note in milliseconds. uint16_t wholenote_; + /// The default duration of a note (e.g. 4 for a quarter note). uint16_t default_duration_; + /// The default octave for a note. uint16_t default_octave_; + /// The time the last note was started. uint32_t last_note_; + /// The duration of the current note in milliseconds. uint16_t note_duration_; + /// The frequency of the current note in Hz. uint32_t output_freq_; + /// The gain of the output. float gain_{0.6f}; + /// The current state of the RTTTL player. State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT + /// The output to write the sound to. output::FloatOutput *output_; #endif #ifdef USE_SPEAKER + /// The speaker to write the sound to. speaker::Speaker *speaker_{nullptr}; + /// The sample rate of the speaker. int sample_rate_{16000}; + /// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format. int samples_per_wave_{0}; + /// The number of samples sent. int samples_sent_{0}; + /// The total number of samples to send. int samples_count_{0}; + /// The number of samples for the gap between notes. int samples_gap_{0}; #endif + /// The callback to call when playback is finished. CallbackManager on_finished_playback_callback_; }; From ba4789970ce08ba779fe492ee356391ce2353b5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:12:38 -0500 Subject: [PATCH 11/21] [esphome] Fix OTA watchdog resets by validating all magic bytes before blocking (#10401) --- .../components/esphome/ota/ota_esphome.cpp | 77 ++++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 8 +- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 5217e9c61f..fc10e5366e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -100,8 +100,8 @@ void ESPHomeOTAComponent::handle_handshake_() { /// Handle the initial OTA handshake. /// /// This method is non-blocking and will return immediately if no data is available. - /// It waits for the first magic byte (0x6C) before proceeding to handle_data_(). - /// A 10-second timeout is enforced from initial connection. + /// It reads all 5 magic bytes (0x6C, 0x26, 0xF7, 0x5C, 0x45) non-blocking + /// before proceeding to handle_data_(). A 10-second timeout is enforced from initial connection. if (this->client_ == nullptr) { // We already checked server_->ready() in loop(), so we can accept directly @@ -126,6 +126,7 @@ void ESPHomeOTAComponent::handle_handshake_() { } this->log_start_("handshake"); this->client_connect_time_ = App.get_loop_component_start_time(); + this->magic_buf_pos_ = 0; // Reset magic buffer position } // Check for handshake timeout @@ -136,34 +137,47 @@ void ESPHomeOTAComponent::handle_handshake_() { return; } - // Try to read first byte of magic bytes - uint8_t first_byte; - ssize_t read = this->client_->read(&first_byte, 1); + // Try to read remaining magic bytes + if (this->magic_buf_pos_ < 5) { + // Read as many bytes as available + uint8_t bytes_to_read = 5 - this->magic_buf_pos_; + ssize_t read = this->client_->read(this->magic_buf_ + this->magic_buf_pos_, bytes_to_read); - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - return; // No data yet, try again next loop - } - - if (read <= 0) { - // Error or connection closed - if (read == -1) { - this->log_socket_error_("reading first byte"); - } else { - ESP_LOGW(TAG, "Remote closed during handshake"); + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop } - this->cleanup_connection_(); - return; + + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_("reading magic bytes"); + } else { + ESP_LOGW(TAG, "Remote closed during handshake"); + } + this->cleanup_connection_(); + return; + } + + this->magic_buf_pos_ += read; } - // Got first byte, check if it's the magic byte - if (first_byte != 0x6C) { - ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); - this->cleanup_connection_(); - return; - } + // Check if we have all 5 magic bytes + if (this->magic_buf_pos_ == 5) { + // Validate magic bytes + static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + if (memcmp(this->magic_buf_, MAGIC_BYTES, 5) != 0) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->magic_buf_[0], + this->magic_buf_[1], this->magic_buf_[2], this->magic_buf_[3], this->magic_buf_[4]); + // Send error response (non-blocking, best effort) + uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); + this->client_->write(&error, 1); + this->cleanup_connection_(); + return; + } - // First byte is valid, continue with data handling - this->handle_data_(); + // All 5 magic bytes are valid, continue with data handling + this->handle_data_(); + } } void ESPHomeOTAComponent::handle_data_() { @@ -186,18 +200,6 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif - // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) - if (!this->readall_(buf, 4)) { - this->log_read_error_("magic bytes"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 - if (buf[0] != 0x26 || buf[1] != 0xF7 || buf[2] != 0x5C || buf[3] != 0x45) { - ESP_LOGW(TAG, "Magic bytes mismatch! 0x6C-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3]); - error_code = ota::OTA_RESPONSE_ERROR_MAGIC; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // Send OK and version - 2 bytes buf[0] = ota::OTA_RESPONSE_OK; buf[1] = USE_OTA_VERSION; @@ -487,6 +489,7 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; this->client_connect_time_ = 0; + this->magic_buf_pos_ = 0; } void ESPHomeOTAComponent::yield_and_feed_watchdog_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 8397b86528..c1919c71e9 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -41,11 +41,13 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::string password_; #endif // USE_OTA_PASSWORD - uint16_t port_; - uint32_t client_connect_time_{0}; - std::unique_ptr server_; std::unique_ptr client_; + + uint32_t client_connect_time_{0}; + uint16_t port_; + uint8_t magic_buf_[5]; + uint8_t magic_buf_pos_{0}; }; } // namespace esphome From 07875a8b1e557a1fbff57f3ba9ce714a20498ab3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:16:19 +1200 Subject: [PATCH 12/21] Bump version to 2025.8.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 24dcc2e859..6e0a1a42e8 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.8.1 +PROJECT_NUMBER = 2025.8.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 555434e4b9..e0e72a6758 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.8.1" +__version__ = "2025.8.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From c526ab9a3f49cfc48632f50639ccb9f3c6af6aab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:20:23 +0000 Subject: [PATCH 13/21] Bump ruff from 0.12.10 to 0.12.11 (#10483) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b76523b2e..e05733ec96 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.10 + rev: v0.12.11 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index f55618c0f8..22f58fd3d7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.10 # also change in .pre-commit-config.yaml when updating +ruff==0.12.11 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 5dc691874b2139e0b6468d4559ed80e1eba3f17e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:30:14 -0500 Subject: [PATCH 14/21] [bluetooth_proxy] Remove unused ClientState::SEARCHING state (#10318) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 16 +++++++------- .../esp32_ble_client/ble_client_base.cpp | 5 ++--- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 21 +++++++------------ .../esp32_ble_tracker/esp32_ble_tracker.h | 9 +------- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 723466a5ff..80b7fbe960 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -183,6 +183,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest this->send_device_connection(msg.address, false); return; } + if (!msg.has_address_type) { + ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), + connection->address_str().c_str()); + this->send_device_connection(msg.address, false); + return; + } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { this->log_connection_request_ignored_(connection, connection->state()); @@ -209,13 +215,9 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); this->log_connection_info_(connection, "v3 without cache"); } - if (msg.has_address_type) { - uint64_to_bd_addr(msg.address, connection->remote_bda_); - connection->set_remote_addr_type(static_cast(msg.address_type)); - connection->set_state(espbt::ClientState::DISCOVERED); - } else { - connection->set_state(espbt::ClientState::SEARCHING); - } + uint64_to_bd_addr(msg.address, connection->remote_bda_); + connection->set_remote_addr_type(static_cast(msg.address_type)); + connection->set_state(espbt::ClientState::DISCOVERED); this->send_connections_free(); break; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 4805855960..af5162afb0 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -93,7 +93,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { return false; if (this->address_ == 0 || device.address_uint64() != this->address_) return false; - if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) + if (this->state_ != espbt::ClientState::IDLE) return false; this->log_event_("Found device"); @@ -168,8 +168,7 @@ void BLEClientBase::unconditional_disconnect() { this->log_gattc_warning_("esp_ble_gattc_close", err); } - if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || - this->state_ == espbt::ClientState::DISCOVERED) { + if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 00bd1fe34c..0edde169eb 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -49,8 +49,6 @@ const char *client_state_to_string(ClientState state) { return "DISCONNECTING"; case ClientState::IDLE: return "IDLE"; - case ClientState::SEARCHING: - return "SEARCHING"; case ClientState::DISCOVERED: return "DISCOVERED"; case ClientState::READY_TO_CONNECT: @@ -136,9 +134,8 @@ void ESP32BLETracker::loop() { ClientStateCounts counts = this->count_client_states_(); if (counts != this->client_state_counts_) { this->client_state_counts_ = counts; - ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", - this->client_state_counts_.connecting, this->client_state_counts_.discovered, - this->client_state_counts_.searching, this->client_state_counts_.disconnecting); + ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); } if (this->scanner_state_ == ScannerState::FAILED || @@ -158,10 +155,8 @@ void ESP32BLETracker::loop() { https://github.com/espressif/esp-idf/issues/6688 */ - bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting; - if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && - !promote_to_connecting) { + if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE this->update_coex_preference_(false); #endif @@ -170,12 +165,11 @@ void ESP32BLETracker::loop() { } } // If there is a discovered client and no connecting - // clients and no clients using the scanner to search for - // devices, then promote the discovered client to ready to connect. + // clients, then promote the discovered client to ready to connect. // We check both RUNNING and IDLE states because: // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler) - if (promote_to_connecting && + if (counts.discovered && !counts.connecting && (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) { this->try_promote_discovered_clients_(); } @@ -633,9 +627,8 @@ void ESP32BLETracker::dump_config() { this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_)); - ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", - this->client_state_counts_.connecting, this->client_state_counts_.discovered, - this->client_state_counts_.searching, this->client_state_counts_.disconnecting); + ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 763fa9f1c6..dd67156108 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -141,12 +141,10 @@ class ESPBTDeviceListener { struct ClientStateCounts { uint8_t connecting = 0; uint8_t discovered = 0; - uint8_t searching = 0; uint8_t disconnecting = 0; bool operator==(const ClientStateCounts &other) const { - return connecting == other.connecting && discovered == other.discovered && searching == other.searching && - disconnecting == other.disconnecting; + return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting; } bool operator!=(const ClientStateCounts &other) const { return !(*this == other); } @@ -159,8 +157,6 @@ enum class ClientState : uint8_t { DISCONNECTING, // Connection is idle, no device detected. IDLE, - // Searching for device. - SEARCHING, // Device advertisement found. DISCOVERED, // Device is discovered and the scanner is stopped @@ -316,9 +312,6 @@ class ESP32BLETracker : public Component, case ClientState::DISCOVERED: counts.discovered++; break; - case ClientState::SEARCHING: - counts.searching++; - break; case ClientState::CONNECTING: case ClientState::READY_TO_CONNECT: counts.connecting++; From cde00a1f4c072ea170c0f5182dc2718dc989c61b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:02:15 +1200 Subject: [PATCH 15/21] Bump esphome-dashboard from 20250814.0 to 20250828.0 (#10484) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2665211381..910f70fe45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 -esphome-dashboard==20250814.0 +esphome-dashboard==20250828.0 aioesphomeapi==39.0.0 zeroconf==0.147.0 puremagic==1.30 From bc960cf6d2b48ea5ace544dcd73fa9dd27fdea20 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:52:37 +1000 Subject: [PATCH 16/21] [mapping] Use custom allocator (#9972) --- esphome/components/mapping/__init__.py | 18 +++-- esphome/components/mapping/mapping.h | 69 ++++++++++++++++++ tests/components/mapping/.gitattributes | 1 + tests/components/mapping/common.yaml | 19 ++++- tests/components/mapping/helvetica.ttf | Bin 0 -> 83644 bytes tests/components/mapping/test.esp32-ard.yaml | 18 ++--- .../components/mapping/test.esp32-c3-ard.yaml | 14 ++-- .../components/mapping/test.esp32-c3-idf.yaml | 14 ++-- tests/components/mapping/test.esp32-idf.yaml | 14 ++-- .../components/mapping/test.esp8266-ard.yaml | 14 ++-- tests/components/mapping/test.host.yaml | 16 ++-- tests/components/mapping/test.rp2040-ard.yaml | 14 ++-- 12 files changed, 152 insertions(+), 59 deletions(-) create mode 100644 esphome/components/mapping/mapping.h create mode 100644 tests/components/mapping/.gitattributes create mode 100644 tests/components/mapping/helvetica.ttf diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 79657084fa..94c7c10a82 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -10,7 +10,8 @@ from esphome.loader import get_component CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True -map_ = cg.std_ns.class_("map") +mapping_ns = cg.esphome_ns.namespace("mapping") +mapping_class = mapping_ns.class_("Mapping") CONF_ENTRIES = "entries" CONF_CLASS = "class" @@ -29,7 +30,11 @@ class IndexType: INDEX_TYPES = { "int": IndexType(cv.int_, cg.int_, int), - "string": IndexType(cv.string, cg.std_string, str), + "string": IndexType( + cv.string, + cg.std_string, + str, + ), } @@ -47,7 +52,7 @@ def to_schema(value): BASE_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(map_), + cv.Required(CONF_ID): cv.declare_id(mapping_class), cv.Required(CONF_FROM): cv.one_of(*INDEX_TYPES, lower=True), cv.Required(CONF_TO): cv.string, }, @@ -123,12 +128,15 @@ async def to_code(config): if list(entries.values())[0].op != ".": value_type = value_type.operator("ptr") varid = config[CONF_ID] - varid.type = map_.template(index_type, value_type) + varid.type = mapping_class.template( + index_type, + value_type, + ) var = MockObj(varid, ".") decl = VariableDeclarationExpression(varid.type, "", varid) add_global(decl) CORE.register_variable(varid, var) for key, value in entries.items(): - cg.add(var.insert((key, value))) + cg.add(var.set(key, value)) return var diff --git a/esphome/components/mapping/mapping.h b/esphome/components/mapping/mapping.h new file mode 100644 index 0000000000..99c1f38829 --- /dev/null +++ b/esphome/components/mapping/mapping.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::mapping { + +using alloc_string_t = std::basic_string, RAMAllocator>; + +/** + * + * Mapping class with custom allocator. + * Additionally, when std::string is used as key or value, it will be replaced with a custom string type + * that uses RAMAllocator. + * @tparam K The type of the key in the mapping. + * @tparam V The type of the value in the mapping. Should be a basic type or pointer. + */ + +static const char *const TAG = "mapping"; + +template class Mapping { + public: + // Constructor + Mapping() = default; + + using key_t = const std::conditional_t, + alloc_string_t, // if K is std::string, custom string type + K>; + using value_t = std::conditional_t, + alloc_string_t, // if V is std::string, custom string type + V>; + + void set(const K &key, const V &value) { this->map_[key_t{key}] = value; } + + V get(const K &key) const { + auto it = this->map_.find(key_t{key}); + if (it != this->map_.end()) { + return V{it->second}; + } + if constexpr (std::is_pointer_v) { + esph_log_e(TAG, "Key '%p' not found in mapping", key); + } else if constexpr (std::is_same_v) { + esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str()); + } else { + esph_log_e(TAG, "Key '%s' not found in mapping", to_string(key).c_str()); + } + return {}; + } + + // index map overload + V operator[](K key) { return this->get(key); } + + // convenience function for strings to get a C-style string + template, int> = 0> + const char *operator[](K key) const { + auto it = this->map_.find(key_t{key}); + if (it != this->map_.end()) { + return it->second.c_str(); // safe since value remains in map + } + return ""; + } + + protected: + std::map, RAMAllocator>> map_; +}; + +} // namespace esphome::mapping diff --git a/tests/components/mapping/.gitattributes b/tests/components/mapping/.gitattributes new file mode 100644 index 0000000000..9d74867fcf --- /dev/null +++ b/tests/components/mapping/.gitattributes @@ -0,0 +1 @@ +*.ttf -text diff --git a/tests/components/mapping/common.yaml b/tests/components/mapping/common.yaml index 07ca458146..7ffcfa4f67 100644 --- a/tests/components/mapping/common.yaml +++ b/tests/components/mapping/common.yaml @@ -50,6 +50,14 @@ mapping: red: red_id blue: blue_id green: green_id + - id: string_map_2 + from: string + to: string + entries: + one: "one" + two: "two" + three: "three" + seventy-seven: "seventy-seven" color: - id: red_id @@ -65,7 +73,14 @@ color: green: 0.0 blue: 1.0 +font: + - file: "$component_dir/helvetica.ttf" + id: font_id + size: 20 + display: lambda: |- - it.image(0, 0, id(weather_map)[0]); - it.image(0, 100, id(weather_map)[1]); + std::string value = id(int_map)[2]; + it.print(0, 0, id(font_id), TextAlign::TOP_LEFT, value.c_str()); + it.image(0, 0, id(weather_map)["clear-night"]); + it.image(0, 100, id(weather_map)["sunny"]); diff --git a/tests/components/mapping/helvetica.ttf b/tests/components/mapping/helvetica.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7aec6f3f3cc74a7138ce350def27a2209b44ad89 GIT binary patch literal 83644 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 Date: Fri, 29 Aug 2025 02:53:54 +0200 Subject: [PATCH 17/21] Update mcp4461.cpp (#10479) --- esphome/components/mcp4461/mcp4461.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 55ce9b7899..191fbae366 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -198,7 +198,7 @@ uint16_t Mcp4461Component::get_wiper_level_(Mcp4461WiperIdx wiper) { uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) { uint8_t addr = this->get_wiper_address_(wiper_idx); - uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); + uint8_t reg = addr | static_cast(Mcp4461Commands::READ); if (wiper_idx > 3) { if (!this->is_eeprom_ready_for_writing_(true)) { return 0; From ef98f67b41dab7bd50e71217b3afa607928f955c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:58:58 +1000 Subject: [PATCH 18/21] [lvgl] Replace spinbox step with selected_digit (#10349) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/widgets/__init__.py | 1 - esphome/components/lvgl/widgets/spinbox.py | 31 +++++++++++++-------- tests/components/lvgl/lvgl-package.yaml | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 8f09a3a6d0..baee403b57 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -451,6 +451,7 @@ CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" CONF_INITIAL_FOCUS = "initial_focus" +CONF_SELECTED_DIGIT = "selected_digit" CONF_KEY_CODE = "key_code" CONF_KEYPADS = "keypads" CONF_LAYOUT = "layout" diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index bb6155234c..1f9cdde0a0 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -67,7 +67,6 @@ class Widget: self.type = wtype self.config = config self.scale = 1.0 - self.step = 1.0 self.range_from = -sys.maxsize self.range_to = sys.maxsize if wtype.is_compound(): diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index b84dc7cd23..26ad149c6f 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -11,6 +11,7 @@ from ..defines import ( CONF_ROLLOVER, CONF_SCROLLBAR, CONF_SELECTED, + CONF_SELECTED_DIGIT, CONF_TEXTAREA_PLACEHOLDER, ) from ..lv_validation import lv_bool, lv_float @@ -38,18 +39,24 @@ def validate_spinbox(config): min_val = -1 - max_val range_from = int(config[CONF_RANGE_FROM]) range_to = int(config[CONF_RANGE_TO]) - step = int(config[CONF_STEP]) + step = config[CONF_SELECTED_DIGIT] + digits = config[CONF_DIGITS] if ( range_from > max_val or range_from < min_val or range_to > max_val or range_to < min_val ): - raise cv.Invalid("Range outside allowed limits") - if step <= 0 or step >= (range_to - range_from) / 2: - raise cv.Invalid("Invalid step value") - if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: - raise cv.Invalid("Number of digits must exceed number of decimal places") + raise cv.Invalid("Range outside allowed limits", path=[CONF_RANGE_FROM]) + if digits <= config[CONF_DECIMAL_PLACES]: + raise cv.Invalid( + "Number of digits must exceed number of decimal places", path=[CONF_DIGITS] + ) + if step >= digits: + raise cv.Invalid( + "Initial selected digit must be less than number of digits", + path=[CONF_SELECTED_DIGIT], + ) return config @@ -59,7 +66,10 @@ SPINBOX_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100): cv.float_, cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), - cv.Optional(CONF_STEP, default=1.0): cv.positive_float, + cv.Optional(CONF_STEP): cv.invalid( + f"{CONF_STEP} has been replaced by {CONF_SELECTED_DIGIT}" + ), + cv.Optional(CONF_SELECTED_DIGIT, default=0): cv.positive_int, cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), cv.Optional(CONF_ROLLOVER, default=False): lv_bool, } @@ -93,13 +103,12 @@ class SpinboxType(WidgetType): scale = 10 ** config[CONF_DECIMAL_PLACES] range_from = int(config[CONF_RANGE_FROM]) * scale range_to = int(config[CONF_RANGE_TO]) * scale - step = int(config[CONF_STEP]) * scale + step = config[CONF_SELECTED_DIGIT] w.scale = scale - w.step = step w.range_to = range_to w.range_from = range_from lv.spinbox_set_range(w.obj, range_from, range_to) - await w.set_property(CONF_STEP, step) + await w.set_property("step", 10**step) await w.set_property(CONF_ROLLOVER, config) lv.spinbox_set_digit_format( w.obj, digits, digits - config[CONF_DECIMAL_PLACES] @@ -120,7 +129,7 @@ class SpinboxType(WidgetType): return config[CONF_RANGE_FROM] def get_step(self, config: dict): - return config[CONF_STEP] + return 10 ** config[CONF_SELECTED_DIGIT] spinbox_spec = SpinboxType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index feee96672c..582531e943 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -684,7 +684,7 @@ lvgl: width: 120 range_from: -10 range_to: 1000 - step: 5.0 + selected_digit: 2 rollover: false digits: 6 decimal_places: 2 From dea68bebd8f41c9ab4d0ee3d3fa38fc7b679685e Mon Sep 17 00:00:00 2001 From: Ben Curtis Date: Thu, 28 Aug 2025 22:00:54 -0400 Subject: [PATCH 19/21] Adjust sen5x to match VOC/NOX datasheet (#9894) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sen5x/sensor.py | 111 +++++++++++++++++------------ 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index f52de5fe85..9668a253c0 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -65,26 +65,47 @@ ACCELERATION_MODES = { "high": RhtAccelerationMode.HIGH_ACCELERATION, } -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( - 1, 1000 - ), - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( - 1, 1000 - ), - cv.Optional( - CONF_GATING_MAX_DURATION_MINUTES, default=720 - ): cv.int_range(0, 3000), - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), - } - ) - } -) + +def _gas_sensor( + *, + index_offset: int, + learning_time_offset: int, + learning_time_gain: int, + gating_max_duration: int, + std_initial: int, + gain_factor: int, +) -> cv.Schema: + return sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=index_offset): cv.int_range( + 1, 250 + ), + cv.Optional( + CONF_LEARNING_TIME_OFFSET_HOURS, default=learning_time_offset + ): cv.int_range(1, 1000), + cv.Optional( + CONF_LEARNING_TIME_GAIN_HOURS, default=learning_time_gain + ): cv.int_range(1, 1000), + cv.Optional( + CONF_GATING_MAX_DURATION_MINUTES, default=gating_max_duration + ): cv.int_range(0, 3000), + cv.Optional(CONF_STD_INITIAL, default=std_initial): cv.int_range( + 10, 5000 + ), + cv.Optional(CONF_GAIN_FACTOR, default=gain_factor): cv.int_range( + 1, 1000 + ), + } + ) + } + ) def float_previously_pct(value): @@ -127,18 +148,22 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, - cv.Optional(CONF_VOC): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), - cv.Optional(CONF_NOX): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + cv.Optional(CONF_VOC): _gas_sensor( + index_offset=100, + learning_time_offset=12, + learning_time_gain=12, + gating_max_duration=180, + std_initial=50, + gain_factor=230, + ), + cv.Optional(CONF_NOX): _gas_sensor( + index_offset=1, + learning_time_offset=12, + learning_time_gain=12, + gating_max_duration=720, + std_initial=50, + gain_factor=230, + ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( @@ -194,16 +219,15 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for key, funcName in SETTING_MAP.items(): - if key in config: - cg.add(getattr(var, funcName)(config[key])) + if cfg := config.get(key): + cg.add(getattr(var, funcName)(cfg)) for key, funcName in SENSOR_MAP.items(): - if key in config: - sens = await sensor.new_sensor(config[key]) + if cfg := config.get(key): + sens = await sensor.new_sensor(cfg) cg.add(getattr(var, funcName)(sens)) - if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]: - cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + if cfg := config.get(CONF_VOC, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_voc_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -214,8 +238,7 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]: - cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + if cfg := config.get(CONF_NOX, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_nox_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -225,12 +248,12 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if CONF_TEMPERATURE_COMPENSATION in config: + if cfg := config.get(CONF_TEMPERATURE_COMPENSATION): cg.add( var.set_temperature_compensation( - config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], - config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], - config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], + cfg[CONF_OFFSET], + cfg[CONF_NORMALIZED_OFFSET_SLOPE], + cfg[CONF_TIME_CONSTANT], ) ) From ca72286386aa0f47e609bd3774cab65ccc4047c5 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:42:39 +1000 Subject: [PATCH 20/21] [lvgl] Update hello world (#10469) --- esphome/components/lvgl/hello_world.py | 129 ++++++++++++++++++------- 1 file changed, 96 insertions(+), 33 deletions(-) diff --git a/esphome/components/lvgl/hello_world.py b/esphome/components/lvgl/hello_world.py index 2c2ec6732c..f85da9d8e4 100644 --- a/esphome/components/lvgl/hello_world.py +++ b/esphome/components/lvgl/hello_world.py @@ -4,49 +4,112 @@ from esphome.yaml_util import parse_yaml CONFIG = """ - obj: - radius: 0 + id: hello_world_card_ pad_all: 12 - bg_color: 0xFFFFFF + bg_color: white height: 100% width: 100% + scrollable: false widgets: - - spinner: - id: hello_world_spinner_ - align: center - indicator: - arc_color: tomato - height: 100 - width: 100 - spin_time: 2s - arc_length: 60deg - - label: - id: hello_world_label_ - text: "Hello World!" + - obj: + align: top_mid + outline_width: 0 + border_width: 0 + pad_all: 4 + scrollable: false + height: size_content + width: 100% + layout: + type: flex + flex_flow: row + flex_align_cross: center + flex_align_track: start + flex_align_main: space_between + widgets: + - button: + checkable: true + radius: 4 + text_font: montserrat_20 + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Clicked!" + widgets: + - label: + text: "Button" + - label: + id: hello_world_title_ + text: ESPHome + text_font: montserrat_20 + width: 100% + text_align: center + on_boot: + lvgl.widget.refresh: hello_world_title_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 400; + - checkbox: + text: Checkbox + id: hello_world_checkbox_ + on_boot: + lvgl.widget.refresh: hello_world_checkbox_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 240; + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Checked!" + - obj: + id: hello_world_container_ align: center + y: 14 + pad_all: 0 + outline_width: 0 + border_width: 0 + width: 100% + height: size_content + scrollable: false on_click: lvgl.spinner.update: id: hello_world_spinner_ arc_color: springgreen - - checkbox: - pad_all: 8 - text: Checkbox - align: top_right - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Checked!" - - button: - pad_all: 8 - checkable: true - align: top_left - text_font: montserrat_20 - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Clicked!" + layout: + type: flex + flex_flow: row_wrap + flex_align_cross: center + flex_align_track: center + flex_align_main: space_evenly widgets: - - label: - text: "Button" + - spinner: + id: hello_world_spinner_ + indicator: + arc_color: tomato + height: 100 + width: 100 + spin_time: 2s + arc_length: 60deg + widgets: + - label: + id: hello_world_label_ + text: "Hello World!" + align: center + - obj: + id: hello_world_qrcode_ + outline_width: 0 + border_width: 0 + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + widgets: + - label: + text_font: montserrat_14 + text: esphome.io + align: top_mid + - qrcode: + text: "https://esphome.io" + size: 80 + align: bottom_mid + on_boot: + lvgl.widget.refresh: hello_world_qrcode_ + - slider: width: 80% align: bottom_mid From a6eaf59effc816e9eaabbd39b6d0da9300f872e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 08:59:09 -0500 Subject: [PATCH 21/21] [bluetooth_proxy] Expose configured scanning mode in API responses --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 3 ++- esphome/components/api/api_pb2_dump.cpp | 1 + esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 6 ++++++ esphome/components/bluetooth_proxy/bluetooth_proxy.h | 3 ++- 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 6b19f2026a..9707e714e7 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1712,6 +1712,7 @@ message BluetoothScannerStateResponse { BluetoothScannerState state = 1; BluetoothScannerMode mode = 2; + BluetoothScannerMode configured_mode = 3; } message BluetoothScannerSetModeRequest { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 476e3c88d0..de60ed3fdb 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2153,10 +2153,12 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); + buffer.encode_uint32(3, static_cast(this->configured_mode)); } void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->state)); size.add_uint32(1, static_cast(this->mode)); + size.add_uint32(1, static_cast(this->configured_mode)); } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index abdf0e6121..3f2c2ea763 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2214,12 +2214,13 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { class BluetoothScannerStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 126; - static constexpr uint8_t ESTIMATED_SIZE = 4; + static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; + enums::BluetoothScannerMode configured_mode{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 7af322f96d..3e7df9195b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1704,6 +1704,7 @@ void BluetoothScannerStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); dump_field(out, "state", static_cast(this->state)); dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "configured_mode", static_cast(this->configured_mode)); } void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 80b7fbe960..532aff550e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -24,6 +24,9 @@ void BluetoothProxy::setup() { this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; + // Capture the configured scan mode from YAML before any API changes + this->configured_scan_active_ = this->parent_->get_scan_active(); + this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { this->send_bluetooth_scanner_state_(state); @@ -36,6 +39,9 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.state = static_cast(state); resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; + resp.configured_mode = this->configured_scan_active_ + ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE + : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index c81c8c9532..4b262dbe86 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -161,7 +161,8 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ // Group 4: 1-byte types grouped together bool active_; uint8_t connection_count_{0}; - // 2 bytes used, 2 bytes padding + bool configured_scan_active_{false}; // Configured scan mode from YAML + // 3 bytes used, 1 byte padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)