mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			49 Commits
		
	
	
		
			2025.8.0b2
			...
			2025.8.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6a20e6f9ad | ||
| 
						 | 
					07875a8b1e | ||
| 
						 | 
					ba4789970c | ||
| 
						 | 
					015977cfdf | ||
| 
						 | 
					e513c0f004 | ||
| 
						 | 
					a11970aee0 | ||
| 
						 | 
					4ab37b069b | ||
| 
						 | 
					b6bb6699d1 | ||
| 
						 | 
					078eaff9a8 | ||
| 
						 | 
					4dc11f05a7 | ||
| 
						 | 
					2aceb56606 | ||
| 
						 | 
					d071a074ef | ||
| 
						 | 
					7a459c8c20 | ||
| 
						 | 
					aebd21958a | ||
| 
						 | 
					c542db8bfe | ||
| 
						 | 
					d9dcfe66ec | ||
| 
						 | 
					8517c2e903 | ||
| 
						 | 
					684384892a | ||
| 
						 | 
					d560831d79 | ||
| 
						 | 
					fcc3c8e1b6 | ||
| 
						 | 
					959ffde60e | ||
| 
						 | 
					07715dd50f | ||
| 
						 | 
					03836ee2d2 | ||
| 
						 | 
					50408d9abb | ||
| 
						 | 
					0de7259428 | ||
| 
						 | 
					d054709c2d | ||
| 
						 | 
					da16887915 | ||
| 
						 | 
					2adb993242 | ||
| 
						 | 
					8e67df8059 | ||
| 
						 | 
					c5b2c8d971 | ||
| 
						 | 
					104906ca11 | ||
| 
						 | 
					ad5f6f0cfe | ||
| 
						 | 
					8356f7fcd3 | ||
| 
						 | 
					225de226b0 | ||
| 
						 | 
					fd07e1d979 | ||
| 
						 | 
					23554cda06 | ||
| 
						 | 
					064385eac6 | ||
| 
						 | 
					6502ed70de | ||
| 
						 | 
					bb894c3e32 | ||
| 
						 | 
					c5858b7032 | ||
| 
						 | 
					99f57ecb73 | ||
| 
						 | 
					cc6c892678 | ||
| 
						 | 
					07a98d2525 | ||
| 
						 | 
					e80f616366 | ||
| 
						 | 
					46be877594 | ||
| 
						 | 
					ac8b48a53c | ||
| 
						 | 
					7fdbd8528a | ||
| 
						 | 
					80970f972b | ||
| 
						 | 
					3c7865cd6f | 
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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.0b2
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -476,7 +479,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
 | 
			
		||||
        from esphome.components.api.client import run_logs
 | 
			
		||||
 | 
			
		||||
        return run_logs(config, addresses_to_use)
 | 
			
		||||
    if get_port_type(port) == "MQTT" and "mqtt" in config:
 | 
			
		||||
    if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
        return mqtt.show_logs(
 | 
			
		||||
 
 | 
			
		||||
@@ -321,6 +321,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
 | 
			
		||||
    HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, True)
 | 
			
		||||
    cg.add(var.set_service("esphome.tag_scanned"))
 | 
			
		||||
 
 | 
			
		||||
@@ -455,9 +455,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
 | 
			
		||||
  resp.cold_white = values.get_cold_white();
 | 
			
		||||
  resp.warm_white = values.get_warm_white();
 | 
			
		||||
  if (light->supports_effects()) {
 | 
			
		||||
    // get_effect_name() returns temporary std::string - must store it
 | 
			
		||||
    std::string effect_name = light->get_effect_name();
 | 
			
		||||
    resp.set_effect(StringRef(effect_name));
 | 
			
		||||
    resp.set_effect(light->get_effect_name_ref());
 | 
			
		||||
  }
 | 
			
		||||
  return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
@@ -1415,9 +1413,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
 | 
			
		||||
  static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
 | 
			
		||||
  resp.set_esphome_version(ESPHOME_VERSION_REF);
 | 
			
		||||
 | 
			
		||||
  // get_compilation_time() returns temporary std::string - must store it
 | 
			
		||||
  std::string compilation_time = App.get_compilation_time();
 | 
			
		||||
  resp.set_compilation_time(StringRef(compilation_time));
 | 
			
		||||
  resp.set_compilation_time(App.get_compilation_time_ref());
 | 
			
		||||
 | 
			
		||||
  // Compile-time StringRef constants for manufacturers
 | 
			
		||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
 | 
			
		||||
 
 | 
			
		||||
@@ -382,20 +382,15 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
 | 
			
		||||
// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H)
 | 
			
		||||
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
 | 
			
		||||
// Default is 143FH (20ms, 63ms)
 | 
			
		||||
uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) {
 | 
			
		||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1us is plenty
 | 
			
		||||
  uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
 | 
			
		||||
  uint8_t addrl = (a_register & 0xFF);
 | 
			
		||||
  uint8_t data[4] = {addrh, addrl, 0x00, 0x00};
 | 
			
		||||
  this->transfer_array(data, 4);
 | 
			
		||||
  uint16_t output = encode_uint16(data[2], data[3]);
 | 
			
		||||
  ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1us is plenty
 | 
			
		||||
  uint16_t output = this->read16_transaction_(a_register);
 | 
			
		||||
  delay_microseconds_safe(1);  // allow the last clock to propagate before releasing CS
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);  // meet minimum CS high time before next transaction
 | 
			
		||||
@@ -403,14 +398,8 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_h = this->read16_transaction_(addr_h);
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_l = this->read16_transaction_(addr_l);
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_h = this->read16_(addr_h);
 | 
			
		||||
  const uint16_t val_l = this->read16_(addr_l);
 | 
			
		||||
  const int32_t val = (val_h << 16) | val_l;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG,
 | 
			
		||||
 
 | 
			
		||||
@@ -140,7 +140,6 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  uint16_t read16_(uint16_t a_register);
 | 
			
		||||
  uint16_t read16_transaction_(uint16_t a_register);
 | 
			
		||||
  int read32_(uint16_t addr_h, uint16_t addr_l);
 | 
			
		||||
  void write16_(uint16_t a_register, uint16_t val, bool validate = true);
 | 
			
		||||
  float get_local_phase_voltage_(uint8_t phase);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -133,7 +133,7 @@ void BluetoothConnection::loop() {
 | 
			
		||||
 | 
			
		||||
  // Check if we should disable the loop
 | 
			
		||||
  // - For V3_WITH_CACHE: Services are never sent, disable after INIT state
 | 
			
		||||
  // - For other connections: Disable only after service discovery is complete
 | 
			
		||||
  // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
 | 
			
		||||
  //   (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
 | 
			
		||||
  if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
 | 
			
		||||
                                                   this->send_service_ == DONE_SENDING_SERVICES)) {
 | 
			
		||||
@@ -160,10 +160,7 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
  if (this->send_service_ >= this->service_count_) {
 | 
			
		||||
    this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
    this->proxy_->send_gatt_services_done(this->address_);
 | 
			
		||||
    if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
 | 
			
		||||
        this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
      this->release_services();
 | 
			
		||||
    }
 | 
			
		||||
    this->release_services();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -378,10 +375,19 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->reset_connection_(param->disconnect.reason);
 | 
			
		||||
      // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
 | 
			
		||||
      // This prevents race condition where we mark slot as free before controller cleanup is complete
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               param->disconnect.reason);
 | 
			
		||||
      // Send disconnection notification but don't free the slot yet
 | 
			
		||||
      this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               param->close.reason);
 | 
			
		||||
      // Now the GATT connection is fully closed and controller resources are freed
 | 
			
		||||
      // Safe to mark the connection slot as available
 | 
			
		||||
      this->reset_connection_(param->close.reason);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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); }
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component,
 | 
			
		||||
                           public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
 | 
			
		||||
                                                 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
 | 
			
		||||
  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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#include "soc/soc_caps.h"
 | 
			
		||||
#include "driver/gpio.h"
 | 
			
		||||
#include "deep_sleep_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
@@ -83,7 +84,11 @@ void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
    }
 | 
			
		||||
    gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
 | 
			
		||||
    gpio_hold_en(gpio_pin);
 | 
			
		||||
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
 | 
			
		||||
    // Some ESP32 variants support holding a single GPIO during deep sleep without this function
 | 
			
		||||
    // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
 | 
			
		||||
    gpio_deep_sleep_hold_en();
 | 
			
		||||
#endif
 | 
			
		||||
    bool level = !this->wakeup_pin_->is_inverted();
 | 
			
		||||
    if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
 | 
			
		||||
      level = !level;
 | 
			
		||||
@@ -120,7 +125,11 @@ void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
    }
 | 
			
		||||
    gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
 | 
			
		||||
    gpio_hold_en(gpio_pin);
 | 
			
		||||
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
 | 
			
		||||
    // Some ESP32 variants support holding a single GPIO during deep sleep without this function
 | 
			
		||||
    // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
 | 
			
		||||
    gpio_deep_sleep_hold_en();
 | 
			
		||||
#endif
 | 
			
		||||
    bool level = !this->wakeup_pin_->is_inverted();
 | 
			
		||||
    if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
 | 
			
		||||
      level = !level;
 | 
			
		||||
 
 | 
			
		||||
@@ -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_() {
 | 
			
		||||
 
 | 
			
		||||
@@ -824,8 +824,9 @@ async def to_code(config):
 | 
			
		||||
    cg.set_cpp_standard("gnu++20")
 | 
			
		||||
    cg.add_build_flag("-DUSE_ESP32")
 | 
			
		||||
    cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
 | 
			
		||||
    cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
 | 
			
		||||
    cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
 | 
			
		||||
    variant = config[CONF_VARIANT]
 | 
			
		||||
    cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
 | 
			
		||||
    cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
 | 
			
		||||
    cg.add_define(ThreadModel.MULTI_ATOMICS)
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("lib_ldf_mode", "off")
 | 
			
		||||
@@ -859,6 +860,7 @@ async def to_code(config):
 | 
			
		||||
        cg.add_platformio_option(
 | 
			
		||||
            "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"]
 | 
			
		||||
        )
 | 
			
		||||
        add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -280,6 +280,10 @@ async def to_code(config):
 | 
			
		||||
            add_idf_sdkconfig_option(
 | 
			
		||||
                "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
 | 
			
		||||
            )
 | 
			
		||||
            # Increase GATT client connection retry count for problematic devices
 | 
			
		||||
            # Default in ESP-IDF is 3, we increase to 10 for better reliability with
 | 
			
		||||
            # low-power/timing-sensitive devices
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
 | 
			
		||||
 | 
			
		||||
        # Set the maximum number of notification registrations
 | 
			
		||||
        # This controls how many BLE characteristics can have notifications enabled
 | 
			
		||||
@@ -294,6 +298,7 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    if config[CONF_ADVERTISING]:
 | 
			
		||||
        cg.add_define("USE_ESP32_BLE_ADVERTISING")
 | 
			
		||||
        cg.add_define("USE_ESP32_BLE_UUID")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
 | 
			
		||||
 
 | 
			
		||||
@@ -306,7 +306,7 @@ void ESP32BLE::loop() {
 | 
			
		||||
      case BLEEvent::GATTS: {
 | 
			
		||||
        esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
 | 
			
		||||
        esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
 | 
			
		||||
        esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param;
 | 
			
		||||
        esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param;
 | 
			
		||||
        ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
 | 
			
		||||
        for (auto *gatts_handler : this->gatts_event_handlers_) {
 | 
			
		||||
          gatts_handler->gatts_event_handler(event, gatts_if, param);
 | 
			
		||||
@@ -316,7 +316,7 @@ void ESP32BLE::loop() {
 | 
			
		||||
      case BLEEvent::GATTC: {
 | 
			
		||||
        esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
 | 
			
		||||
        esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
 | 
			
		||||
        esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param;
 | 
			
		||||
        esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param;
 | 
			
		||||
        ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
 | 
			
		||||
        for (auto *gattc_handler : this->gattc_event_handlers_) {
 | 
			
		||||
          gattc_handler->gattc_event_handler(event, gattc_if, param);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,7 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <cstddef>  // for offsetof
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include <cstring>  // for memcpy
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <esp_gatts_api.h>
 | 
			
		||||
@@ -62,10 +61,24 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(es
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
 | 
			
		||||
              "remote_addr must follow rssi in read_rssi_cmpl");
 | 
			
		||||
 | 
			
		||||
// Param struct sizes on ESP32
 | 
			
		||||
static constexpr size_t GATTC_PARAM_SIZE = 28;
 | 
			
		||||
static constexpr size_t GATTS_PARAM_SIZE = 32;
 | 
			
		||||
 | 
			
		||||
// Maximum size for inline storage of data
 | 
			
		||||
// GATTC: 80 - 28 (param) - 8 (other fields) = 44 bytes for data
 | 
			
		||||
// GATTS: 80 - 32 (param) - 8 (other fields) = 40 bytes for data
 | 
			
		||||
static constexpr size_t GATTC_INLINE_DATA_SIZE = 44;
 | 
			
		||||
static constexpr size_t GATTS_INLINE_DATA_SIZE = 40;
 | 
			
		||||
 | 
			
		||||
// Verify param struct sizes
 | 
			
		||||
static_assert(sizeof(esp_ble_gattc_cb_param_t) == GATTC_PARAM_SIZE, "GATTC param size unexpected");
 | 
			
		||||
static_assert(sizeof(esp_ble_gatts_cb_param_t) == GATTS_PARAM_SIZE, "GATTS param size unexpected");
 | 
			
		||||
 | 
			
		||||
// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
 | 
			
		||||
// This class stores each event with minimal memory usage.
 | 
			
		||||
// GAP events (99% of traffic) don't have the vector overhead.
 | 
			
		||||
// GATTC/GATTS events use heap allocation for their param and data.
 | 
			
		||||
// GAP events (99% of traffic) don't have the heap allocation overhead.
 | 
			
		||||
// GATTC/GATTS events use heap allocation for their param and inline storage for small data.
 | 
			
		||||
//
 | 
			
		||||
// Event flow:
 | 
			
		||||
// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
 | 
			
		||||
@@ -112,21 +125,21 @@ class BLEEvent {
 | 
			
		||||
    this->init_gap_data_(e, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Constructor for GATTC events - uses heap allocation
 | 
			
		||||
  // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
 | 
			
		||||
  // The param pointer from ESP-IDF is only valid during the callback execution.
 | 
			
		||||
  // Since BLE events are processed asynchronously in the main loop, we must create
 | 
			
		||||
  // our own copy to ensure the data remains valid until the event is processed.
 | 
			
		||||
  // Constructor for GATTC events - param stored inline, data may use heap
 | 
			
		||||
  // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF
 | 
			
		||||
  // is only valid during the callback execution. Since BLE events are processed
 | 
			
		||||
  // asynchronously in the main loop, we store our own copy inline to ensure
 | 
			
		||||
  // the data remains valid until the event is processed.
 | 
			
		||||
  BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
 | 
			
		||||
    this->type_ = GATTC;
 | 
			
		||||
    this->init_gattc_data_(e, i, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Constructor for GATTS events - uses heap allocation
 | 
			
		||||
  // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
 | 
			
		||||
  // The param pointer from ESP-IDF is only valid during the callback execution.
 | 
			
		||||
  // Since BLE events are processed asynchronously in the main loop, we must create
 | 
			
		||||
  // our own copy to ensure the data remains valid until the event is processed.
 | 
			
		||||
  // Constructor for GATTS events - param stored inline, data may use heap
 | 
			
		||||
  // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF
 | 
			
		||||
  // is only valid during the callback execution. Since BLE events are processed
 | 
			
		||||
  // asynchronously in the main loop, we store our own copy inline to ensure
 | 
			
		||||
  // the data remains valid until the event is processed.
 | 
			
		||||
  BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
 | 
			
		||||
    this->type_ = GATTS;
 | 
			
		||||
    this->init_gatts_data_(e, i, p);
 | 
			
		||||
@@ -136,25 +149,32 @@ class BLEEvent {
 | 
			
		||||
  ~BLEEvent() { this->release(); }
 | 
			
		||||
 | 
			
		||||
  // Default constructor for pre-allocation in pool
 | 
			
		||||
  BLEEvent() : type_(GAP) {}
 | 
			
		||||
  BLEEvent() : event_{}, type_(GAP) {}
 | 
			
		||||
 | 
			
		||||
  // Invoked on return to EventPool - clean up any heap-allocated data
 | 
			
		||||
  void release() {
 | 
			
		||||
    if (this->type_ == GAP) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->type_ == GATTC) {
 | 
			
		||||
      delete this->event_.gattc.gattc_param;
 | 
			
		||||
      delete this->event_.gattc.data;
 | 
			
		||||
      this->event_.gattc.gattc_param = nullptr;
 | 
			
		||||
      this->event_.gattc.data = nullptr;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->type_ == GATTS) {
 | 
			
		||||
      delete this->event_.gatts.gatts_param;
 | 
			
		||||
      delete this->event_.gatts.data;
 | 
			
		||||
      this->event_.gatts.gatts_param = nullptr;
 | 
			
		||||
      this->event_.gatts.data = nullptr;
 | 
			
		||||
    switch (this->type_) {
 | 
			
		||||
      case GAP:
 | 
			
		||||
        // GAP events don't have heap allocations
 | 
			
		||||
        break;
 | 
			
		||||
      case GATTC:
 | 
			
		||||
        // Param is now stored inline, only delete heap data if it was heap-allocated
 | 
			
		||||
        if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) {
 | 
			
		||||
          delete[] this->event_.gattc.data.heap_data;
 | 
			
		||||
        }
 | 
			
		||||
        // Clear critical fields to prevent issues if type changes
 | 
			
		||||
        this->event_.gattc.is_inline = false;
 | 
			
		||||
        this->event_.gattc.data.heap_data = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
      case GATTS:
 | 
			
		||||
        // Param is now stored inline, only delete heap data if it was heap-allocated
 | 
			
		||||
        if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) {
 | 
			
		||||
          delete[] this->event_.gatts.data.heap_data;
 | 
			
		||||
        }
 | 
			
		||||
        // Clear critical fields to prevent issues if type changes
 | 
			
		||||
        this->event_.gatts.is_inline = false;
 | 
			
		||||
        this->event_.gatts.data.heap_data = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -206,20 +226,30 @@ class BLEEvent {
 | 
			
		||||
 | 
			
		||||
    // NOLINTNEXTLINE(readability-identifier-naming)
 | 
			
		||||
    struct gattc_event {
 | 
			
		||||
      esp_gattc_cb_event_t gattc_event;
 | 
			
		||||
      esp_gatt_if_t gattc_if;
 | 
			
		||||
      esp_ble_gattc_cb_param_t *gattc_param;  // Heap-allocated
 | 
			
		||||
      std::vector<uint8_t> *data;             // Heap-allocated
 | 
			
		||||
    } gattc;                                  // 16 bytes (pointers only)
 | 
			
		||||
      esp_ble_gattc_cb_param_t gattc_param;  // Stored inline (28 bytes)
 | 
			
		||||
      esp_gattc_cb_event_t gattc_event;      // 4 bytes
 | 
			
		||||
      union {
 | 
			
		||||
        uint8_t *heap_data;                           // 4 bytes when heap-allocated
 | 
			
		||||
        uint8_t inline_data[GATTC_INLINE_DATA_SIZE];  // 44 bytes when stored inline
 | 
			
		||||
      } data;                                         // 44 bytes total
 | 
			
		||||
      uint16_t data_len;                              // 2 bytes
 | 
			
		||||
      esp_gatt_if_t gattc_if;                         // 1 byte
 | 
			
		||||
      bool is_inline;                                 // 1 byte - true when data is stored inline
 | 
			
		||||
    } gattc;                                          // Total: 80 bytes
 | 
			
		||||
 | 
			
		||||
    // NOLINTNEXTLINE(readability-identifier-naming)
 | 
			
		||||
    struct gatts_event {
 | 
			
		||||
      esp_gatts_cb_event_t gatts_event;
 | 
			
		||||
      esp_gatt_if_t gatts_if;
 | 
			
		||||
      esp_ble_gatts_cb_param_t *gatts_param;  // Heap-allocated
 | 
			
		||||
      std::vector<uint8_t> *data;             // Heap-allocated
 | 
			
		||||
    } gatts;                                  // 16 bytes (pointers only)
 | 
			
		||||
  } event_;                                   // 80 bytes
 | 
			
		||||
      esp_ble_gatts_cb_param_t gatts_param;  // Stored inline (32 bytes)
 | 
			
		||||
      esp_gatts_cb_event_t gatts_event;      // 4 bytes
 | 
			
		||||
      union {
 | 
			
		||||
        uint8_t *heap_data;                           // 4 bytes when heap-allocated
 | 
			
		||||
        uint8_t inline_data[GATTS_INLINE_DATA_SIZE];  // 40 bytes when stored inline
 | 
			
		||||
      } data;                                         // 40 bytes total
 | 
			
		||||
      uint16_t data_len;                              // 2 bytes
 | 
			
		||||
      esp_gatt_if_t gatts_if;                         // 1 byte
 | 
			
		||||
      bool is_inline;                                 // 1 byte - true when data is stored inline
 | 
			
		||||
    } gatts;                                          // Total: 80 bytes
 | 
			
		||||
  } event_;                                           // 80 bytes
 | 
			
		||||
 | 
			
		||||
  ble_event_t type_;
 | 
			
		||||
 | 
			
		||||
@@ -233,6 +263,29 @@ class BLEEvent {
 | 
			
		||||
  const esp_ble_sec_t &security() const { return event_.gap.security; }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  // Helper to copy data with inline storage optimization
 | 
			
		||||
  template<typename EventStruct, size_t InlineSize>
 | 
			
		||||
  void copy_data_with_inline_storage_(EventStruct &event, const uint8_t *src_data, uint16_t len,
 | 
			
		||||
                                      uint8_t **param_value_ptr) {
 | 
			
		||||
    event.data_len = len;
 | 
			
		||||
    if (len > 0) {
 | 
			
		||||
      if (len <= InlineSize) {
 | 
			
		||||
        event.is_inline = true;
 | 
			
		||||
        memcpy(event.data.inline_data, src_data, len);
 | 
			
		||||
        *param_value_ptr = event.data.inline_data;
 | 
			
		||||
      } else {
 | 
			
		||||
        event.is_inline = false;
 | 
			
		||||
        event.data.heap_data = new uint8_t[len];
 | 
			
		||||
        memcpy(event.data.heap_data, src_data, len);
 | 
			
		||||
        *param_value_ptr = event.data.heap_data;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      event.is_inline = false;
 | 
			
		||||
      event.data.heap_data = nullptr;
 | 
			
		||||
      *param_value_ptr = nullptr;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize GAP event data
 | 
			
		||||
  void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
 | 
			
		||||
    this->event_.gap.gap_event = e;
 | 
			
		||||
@@ -317,35 +370,38 @@ class BLEEvent {
 | 
			
		||||
    this->event_.gattc.gattc_if = i;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      this->event_.gattc.gattc_param = nullptr;
 | 
			
		||||
      this->event_.gattc.data = nullptr;
 | 
			
		||||
      // Zero out the param struct when null
 | 
			
		||||
      memset(&this->event_.gattc.gattc_param, 0, sizeof(this->event_.gattc.gattc_param));
 | 
			
		||||
      this->event_.gattc.is_inline = false;
 | 
			
		||||
      this->event_.gattc.data.heap_data = nullptr;
 | 
			
		||||
      this->event_.gattc.data_len = 0;
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Heap-allocate param and data
 | 
			
		||||
    // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
 | 
			
		||||
    // while GAP events (99%) are stored inline to minimize memory usage
 | 
			
		||||
    // IMPORTANT: This heap allocation provides clear ownership semantics:
 | 
			
		||||
    // - The BLEEvent owns the allocated memory for its lifetime
 | 
			
		||||
    // - The data remains valid from the BLE callback context until processed in the main loop
 | 
			
		||||
    // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
 | 
			
		||||
    this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
 | 
			
		||||
    // Copy param struct inline (no heap allocation!)
 | 
			
		||||
    // GATTC/GATTS events are rare (<1% of events) but we can still store them inline
 | 
			
		||||
    // along with small data payloads, eliminating all heap allocations for typical BLE operations
 | 
			
		||||
    // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer
 | 
			
		||||
    // is only valid during the callback and will be reused/freed after we return
 | 
			
		||||
    this->event_.gattc.gattc_param = *p;
 | 
			
		||||
 | 
			
		||||
    // Copy data for events that need it
 | 
			
		||||
    // The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
 | 
			
		||||
    // We must copy this data to ensure it remains valid when the event is processed later.
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GATTC_NOTIFY_EVT:
 | 
			
		||||
        this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
 | 
			
		||||
        this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
 | 
			
		||||
        copy_data_with_inline_storage_<decltype(this->event_.gattc), GATTC_INLINE_DATA_SIZE>(
 | 
			
		||||
            this->event_.gattc, p->notify.value, p->notify.value_len, &this->event_.gattc.gattc_param.notify.value);
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GATTC_READ_CHAR_EVT:
 | 
			
		||||
      case ESP_GATTC_READ_DESCR_EVT:
 | 
			
		||||
        this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
 | 
			
		||||
        this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
 | 
			
		||||
        copy_data_with_inline_storage_<decltype(this->event_.gattc), GATTC_INLINE_DATA_SIZE>(
 | 
			
		||||
            this->event_.gattc, p->read.value, p->read.value_len, &this->event_.gattc.gattc_param.read.value);
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->event_.gattc.data = nullptr;
 | 
			
		||||
        this->event_.gattc.is_inline = false;
 | 
			
		||||
        this->event_.gattc.data.heap_data = nullptr;
 | 
			
		||||
        this->event_.gattc.data_len = 0;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -356,30 +412,33 @@ class BLEEvent {
 | 
			
		||||
    this->event_.gatts.gatts_if = i;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      this->event_.gatts.gatts_param = nullptr;
 | 
			
		||||
      this->event_.gatts.data = nullptr;
 | 
			
		||||
      // Zero out the param struct when null
 | 
			
		||||
      memset(&this->event_.gatts.gatts_param, 0, sizeof(this->event_.gatts.gatts_param));
 | 
			
		||||
      this->event_.gatts.is_inline = false;
 | 
			
		||||
      this->event_.gatts.data.heap_data = nullptr;
 | 
			
		||||
      this->event_.gatts.data_len = 0;
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Heap-allocate param and data
 | 
			
		||||
    // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
 | 
			
		||||
    // while GAP events (99%) are stored inline to minimize memory usage
 | 
			
		||||
    // IMPORTANT: This heap allocation provides clear ownership semantics:
 | 
			
		||||
    // - The BLEEvent owns the allocated memory for its lifetime
 | 
			
		||||
    // - The data remains valid from the BLE callback context until processed in the main loop
 | 
			
		||||
    // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
 | 
			
		||||
    this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
 | 
			
		||||
    // Copy param struct inline (no heap allocation!)
 | 
			
		||||
    // GATTC/GATTS events are rare (<1% of events) but we can still store them inline
 | 
			
		||||
    // along with small data payloads, eliminating all heap allocations for typical BLE operations
 | 
			
		||||
    // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer
 | 
			
		||||
    // is only valid during the callback and will be reused/freed after we return
 | 
			
		||||
    this->event_.gatts.gatts_param = *p;
 | 
			
		||||
 | 
			
		||||
    // Copy data for events that need it
 | 
			
		||||
    // The param struct contains pointers (e.g., write.value) that point to temporary buffers.
 | 
			
		||||
    // We must copy this data to ensure it remains valid when the event is processed later.
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GATTS_WRITE_EVT:
 | 
			
		||||
        this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
 | 
			
		||||
        this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
 | 
			
		||||
        copy_data_with_inline_storage_<decltype(this->event_.gatts), GATTS_INLINE_DATA_SIZE>(
 | 
			
		||||
            this->event_.gatts, p->write.value, p->write.len, &this->event_.gatts.gatts_param.write.value);
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->event_.gatts.data = nullptr;
 | 
			
		||||
        this->event_.gatts.is_inline = false;
 | 
			
		||||
        this->event_.gatts.data.heap_data = nullptr;
 | 
			
		||||
        this->event_.gatts.data_len = 0;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -389,6 +448,15 @@ class BLEEvent {
 | 
			
		||||
// The gap member in the union should be 80 bytes (including the gap_event enum)
 | 
			
		||||
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
 | 
			
		||||
 | 
			
		||||
// Verify GATTC and GATTS structs don't exceed GAP struct size
 | 
			
		||||
// This ensures the union size is determined by GAP (the most common event type)
 | 
			
		||||
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gattc)) <=
 | 
			
		||||
                  sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)),
 | 
			
		||||
              "gattc_event struct exceeds gap_event size - union size would increase");
 | 
			
		||||
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gatts)) <=
 | 
			
		||||
                  sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)),
 | 
			
		||||
              "gatts_event struct exceeds gap_event size - union size would increase");
 | 
			
		||||
 | 
			
		||||
// Verify esp_ble_sec_t fits within our union
 | 
			
		||||
static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
#include <esp_gatt_defs.h>
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
@@ -111,43 +112,19 @@ void BLEClientBase::connect() {
 | 
			
		||||
           this->remote_addr_type_);
 | 
			
		||||
  this->paired_ = false;
 | 
			
		||||
 | 
			
		||||
  // Set preferred connection parameters before connecting
 | 
			
		||||
  // Use FAST for all V3 connections (better latency and reliability)
 | 
			
		||||
  // Use MEDIUM for V1/legacy connections (balanced performance)
 | 
			
		||||
  uint16_t min_interval, max_interval, timeout;
 | 
			
		||||
  const char *param_type;
 | 
			
		||||
 | 
			
		||||
  if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
 | 
			
		||||
      this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
    min_interval = FAST_MIN_CONN_INTERVAL;
 | 
			
		||||
    max_interval = FAST_MAX_CONN_INTERVAL;
 | 
			
		||||
    timeout = FAST_CONN_TIMEOUT;
 | 
			
		||||
    param_type = "fast";
 | 
			
		||||
  } else {
 | 
			
		||||
    min_interval = MEDIUM_MIN_CONN_INTERVAL;
 | 
			
		||||
    max_interval = MEDIUM_MAX_CONN_INTERVAL;
 | 
			
		||||
    timeout = MEDIUM_CONN_TIMEOUT;
 | 
			
		||||
    param_type = "medium";
 | 
			
		||||
  // Determine connection parameters based on connection type
 | 
			
		||||
  if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
    // V3 without cache needs fast params for service discovery
 | 
			
		||||
    this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast");
 | 
			
		||||
  } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
    // V3 with cache can use medium params
 | 
			
		||||
    this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
 | 
			
		||||
  }
 | 
			
		||||
  // For V1/Legacy, don't set params - use ESP-IDF defaults
 | 
			
		||||
 | 
			
		||||
  auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
 | 
			
		||||
                                                      0,  // latency: 0
 | 
			
		||||
                                                      timeout);
 | 
			
		||||
  if (param_ret != ESP_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), param_ret);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->log_connection_params_(param_type);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Now open the connection
 | 
			
		||||
  // Open the connection
 | 
			
		||||
  auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
 | 
			
		||||
  if (ret) {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gattc_open", ret);
 | 
			
		||||
    this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_state(espbt::ClientState::CONNECTING);
 | 
			
		||||
  }
 | 
			
		||||
  this->handle_connection_result_(ret);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
 | 
			
		||||
@@ -159,7 +136,7 @@ void BLEClientBase::disconnect() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_,
 | 
			
		||||
    ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    this->want_disconnect_ = true;
 | 
			
		||||
    return;
 | 
			
		||||
@@ -172,13 +149,11 @@ void BLEClientBase::unconditional_disconnect() {
 | 
			
		||||
  ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
           this->conn_id_);
 | 
			
		||||
  if (this->state_ == espbt::ClientState::DISCONNECTING) {
 | 
			
		||||
    ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    this->log_error_("Already disconnecting");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->conn_id_ == UNSET_CONN_ID) {
 | 
			
		||||
    ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    this->log_error_("conn id unset, cannot disconnect");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
 | 
			
		||||
@@ -234,17 +209,51 @@ void BLEClientBase::log_connection_params_(const char *param_type) {
 | 
			
		||||
  ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::restore_medium_conn_params_() {
 | 
			
		||||
  // Restore to medium connection parameters after initial connection phase
 | 
			
		||||
  // This balances performance with bandwidth usage for normal operation
 | 
			
		||||
void BLEClientBase::handle_connection_result_(esp_err_t ret) {
 | 
			
		||||
  if (ret) {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gattc_open", ret);
 | 
			
		||||
    this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_state(espbt::ClientState::CONNECTING);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_error_(const char *message) {
 | 
			
		||||
  ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_error_(const char *message, int code) {
 | 
			
		||||
  ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_warning_(const char *message) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
 | 
			
		||||
                                        uint16_t timeout, const char *param_type) {
 | 
			
		||||
  esp_ble_conn_update_params_t conn_params = {{0}};
 | 
			
		||||
  memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
 | 
			
		||||
  conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
 | 
			
		||||
  conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
 | 
			
		||||
  conn_params.latency = 0;
 | 
			
		||||
  conn_params.timeout = MEDIUM_CONN_TIMEOUT;
 | 
			
		||||
  this->log_connection_params_("medium");
 | 
			
		||||
  esp_ble_gap_update_conn_params(&conn_params);
 | 
			
		||||
  conn_params.min_int = min_interval;
 | 
			
		||||
  conn_params.max_int = max_interval;
 | 
			
		||||
  conn_params.latency = latency;
 | 
			
		||||
  conn_params.timeout = timeout;
 | 
			
		||||
  this->log_connection_params_(param_type);
 | 
			
		||||
  esp_err_t err = esp_ble_gap_update_conn_params(&conn_params);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gap_update_conn_params", err);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
 | 
			
		||||
                                     const char *param_type) {
 | 
			
		||||
  // Set preferred connection parameters before connecting
 | 
			
		||||
  // These will be used when establishing the connection
 | 
			
		||||
  this->log_connection_params_(param_type);
 | 
			
		||||
  esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
 | 
			
		||||
@@ -264,8 +273,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
                 this->app_id);
 | 
			
		||||
        this->gattc_if_ = esp_gattc_if;
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->reg.app_id, param->reg.status);
 | 
			
		||||
        this->log_error_("gattc app registration failed status", param->reg.status);
 | 
			
		||||
        this->status_ = param->reg.status;
 | 
			
		||||
        this->mark_failed();
 | 
			
		||||
      }
 | 
			
		||||
@@ -277,11 +285,21 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      this->log_gattc_event_("OPEN");
 | 
			
		||||
      // conn_id was already set in ESP_GATTC_CONNECT_EVT
 | 
			
		||||
      this->service_count_ = 0;
 | 
			
		||||
 | 
			
		||||
      // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
 | 
			
		||||
      // error, if the error occurred at the BTA/GATT layer. This can result in the event
 | 
			
		||||
      // arriving after we've already transitioned to IDLE state.
 | 
			
		||||
      if (this->state_ == espbt::ClientState::IDLE) {
 | 
			
		||||
        ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->open.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->state_ != espbt::ClientState::CONNECTING) {
 | 
			
		||||
        // This should not happen but lets log it in case it does
 | 
			
		||||
        // because it means we have a bad assumption about how the
 | 
			
		||||
        // ESP BT stack works.
 | 
			
		||||
        ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
 | 
			
		||||
        ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
 | 
			
		||||
      }
 | 
			
		||||
      if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
 | 
			
		||||
@@ -301,13 +319,14 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      this->set_state(espbt::ClientState::CONNECTED);
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
        // Restore to medium connection parameters for cached connections too
 | 
			
		||||
        this->restore_medium_conn_params_();
 | 
			
		||||
        // Cached connections already connected with medium parameters, no update needed
 | 
			
		||||
        // only set our state, subclients might have more stuff to do yet.
 | 
			
		||||
        this->state_ = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
      // For V3_WITHOUT_CACHE, we already set fast params before connecting
 | 
			
		||||
      // No need to update them again here
 | 
			
		||||
      this->log_event_("Searching for services");
 | 
			
		||||
      esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -332,8 +351,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      // Check if we were disconnected while waiting for service discovery
 | 
			
		||||
      if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
 | 
			
		||||
          this->state_ == espbt::ClientState::CONNECTED) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str());
 | 
			
		||||
        this->log_warning_("Remote closed during discovery");
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->disconnect.reason);
 | 
			
		||||
@@ -389,12 +407,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      if (this->conn_id_ != param->search_cmpl.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("SEARCH_CMPL");
 | 
			
		||||
      // For V3 connections, restore to medium connection parameters after service discovery
 | 
			
		||||
      // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
 | 
			
		||||
      // This balances performance with bandwidth usage after the critical discovery phase
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
 | 
			
		||||
          this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
        this->restore_medium_conn_params_();
 | 
			
		||||
      } else {
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
        this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
 | 
			
		||||
      } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
        for (auto &svc : this->services_) {
 | 
			
		||||
          ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
@@ -506,16 +523,14 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
 | 
			
		||||
        return;
 | 
			
		||||
      esp_bd_addr_t bd_addr;
 | 
			
		||||
      memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               format_hex(bd_addr, 6).c_str());
 | 
			
		||||
      if (!param->ble_security.auth_cmpl.success) {
 | 
			
		||||
        ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
                 param->ble_security.auth_cmpl.fail_reason);
 | 
			
		||||
        this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
 | 
			
		||||
      } else {
 | 
			
		||||
        this->paired_ = true;
 | 
			
		||||
        ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
 | 
			
		||||
                 param->ble_security.auth_cmpl.auth_mode);
 | 
			
		||||
        ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
                 param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -133,10 +133,18 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
 | 
			
		||||
  void log_event_(const char *name);
 | 
			
		||||
  void log_gattc_event_(const char *name);
 | 
			
		||||
  void restore_medium_conn_params_();
 | 
			
		||||
  void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
 | 
			
		||||
                           const char *param_type);
 | 
			
		||||
  void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
 | 
			
		||||
                        const char *param_type);
 | 
			
		||||
  void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
 | 
			
		||||
  void log_gattc_warning_(const char *operation, esp_err_t err);
 | 
			
		||||
  void log_connection_params_(const char *param_type);
 | 
			
		||||
  void handle_connection_result_(esp_err_t ret);
 | 
			
		||||
  // Compact error logging helpers to reduce flash usage
 | 
			
		||||
  void log_error_(const char *message);
 | 
			
		||||
  void log_error_(const char *message, int code);
 | 
			
		||||
  void log_warning_(const char *message);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
 
 | 
			
		||||
@@ -80,14 +80,17 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger<const adv_data_t &>,
 | 
			
		||||
  ESPBTUUID uuid_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  bool parse_device(const ESPBTDevice &device) override { return false; }
 | 
			
		||||
#endif
 | 
			
		||||
  void on_scan_end() override { this->trigger(); }
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
 
 | 
			
		||||
@@ -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<uint8_t>(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_() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<socket::Socket> server_;
 | 
			
		||||
  std::unique_ptr<socket::Socket> client_;
 | 
			
		||||
 | 
			
		||||
  uint32_t client_connect_time_{0};
 | 
			
		||||
  uint16_t port_;
 | 
			
		||||
  uint8_t magic_buf_[5];
 | 
			
		||||
  uint8_t magic_buf_pos_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
#include "http_request_host.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOST
 | 
			
		||||
 | 
			
		||||
#define USE_HTTP_REQUEST_HOST_H
 | 
			
		||||
#define CPPHTTPLIB_NO_EXCEPTIONS
 | 
			
		||||
#include "httplib.h"
 | 
			
		||||
#include "http_request_host.h"
 | 
			
		||||
 | 
			
		||||
#include <regex>
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/components/watchdog/watchdog.h"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "http_request.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOST
 | 
			
		||||
 | 
			
		||||
#define CPPHTTPLIB_NO_EXCEPTIONS
 | 
			
		||||
#include "httplib.h"
 | 
			
		||||
#include "http_request.h"
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace http_request {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,10 @@
 | 
			
		||||
/**
 | 
			
		||||
 * NOTE: This is a copy of httplib.h from https://github.com/yhirose/cpp-httplib
 | 
			
		||||
 *
 | 
			
		||||
 * It has been modified only to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome,
 | 
			
		||||
 * It has been modified to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome,
 | 
			
		||||
 * it was considered preferable to use it with as few changes as possible, to facilitate future updates.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
//  httplib.h
 | 
			
		||||
//
 | 
			
		||||
@@ -17,6 +15,11 @@
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOST
 | 
			
		||||
// Prevent this code being included in main.cpp
 | 
			
		||||
#ifdef USE_HTTP_REQUEST_HOST_H
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifndef CPPHTTPLIB_HTTPLIB_H
 | 
			
		||||
#define CPPHTTPLIB_HTTPLIB_H
 | 
			
		||||
 | 
			
		||||
@@ -9687,5 +9690,6 @@ inline SSL_CTX *Client::ssl_context() const {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#endif  // CPPHTTPLIB_HTTPLIB_H
 | 
			
		||||
#endif  // USE_HTTP_REQUEST_HOST_H
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
#include "i2c.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
@@ -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<const uint8_t *>(&a_register), 2, stop);
 | 
			
		||||
  if (err != ERROR_OK)
 | 
			
		||||
    return err;
 | 
			
		||||
  return bus_->read(address_, data, len);
 | 
			
		||||
  return bus_->write_readv(this->address_, reinterpret_cast<const uint8_t *>(&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<uint8_t> 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<const uint8_t *>(&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<uint8_t> 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<uint16_t[]> temp{new uint16_t[len]};
 | 
			
		||||
  for (size_t i = 0; i < len; i++)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/optional.h"
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#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<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) {
 | 
			
		||||
    std::array<uint8_t, N> 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<uint8_t> 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<uint8_t> &data) {
 | 
			
		||||
  bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#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<uint8_t> 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<uint8_t> 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<std::pair<uint8_t, bool>> scan_results_;  ///< array containing scan results
 | 
			
		||||
  bool scan_{false};                                    ///< Should we scan ? Can be set in the yaml
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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; }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus_esp_idf.h"
 | 
			
		||||
 | 
			
		||||
#include <driver/gpio.h>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
@@ -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<gpio_num_t>(scl_pin_);
 | 
			
		||||
  const gpio_num_t sda_pin = static_cast<gpio_num_t>(sda_pin_);
 | 
			
		||||
  const auto scl_pin = static_cast<gpio_num_t>(scl_pin_);
 | 
			
		||||
  const auto sda_pin = static_cast<gpio_num_t>(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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 <driver/i2c_master.h>
 | 
			
		||||
#else
 | 
			
		||||
#include <driver/i2c.h>
 | 
			
		||||
#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<int>(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;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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_();
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -140,12 +140,22 @@ float LightState::get_setup_priority() const { return setup_priority::HARDWARE -
 | 
			
		||||
void LightState::publish_state() { this->remote_values_callback_.call(); }
 | 
			
		||||
 | 
			
		||||
LightOutput *LightState::get_output() const { return this->output_; }
 | 
			
		||||
 | 
			
		||||
static constexpr const char *EFFECT_NONE = "None";
 | 
			
		||||
static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None");
 | 
			
		||||
 | 
			
		||||
std::string LightState::get_effect_name() {
 | 
			
		||||
  if (this->active_effect_index_ > 0) {
 | 
			
		||||
    return this->effects_[this->active_effect_index_ - 1]->get_name();
 | 
			
		||||
  } else {
 | 
			
		||||
    return "None";
 | 
			
		||||
  }
 | 
			
		||||
  return EFFECT_NONE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
StringRef LightState::get_effect_name_ref() {
 | 
			
		||||
  if (this->active_effect_index_ > 0) {
 | 
			
		||||
    return StringRef(this->effects_[this->active_effect_index_ - 1]->get_name());
 | 
			
		||||
  }
 | 
			
		||||
  return EFFECT_NONE_REF;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LightState::add_new_remote_values_callback(std::function<void()> &&send_callback) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/optional.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/core/string_ref.h"
 | 
			
		||||
#include "light_call.h"
 | 
			
		||||
#include "light_color_values.h"
 | 
			
		||||
#include "light_effect.h"
 | 
			
		||||
@@ -116,6 +117,8 @@ class LightState : public EntityBase, public Component {
 | 
			
		||||
 | 
			
		||||
  /// Return the name of the current effect, or if no effect is active "None".
 | 
			
		||||
  std::string get_effect_name();
 | 
			
		||||
  /// Return the name of the current effect as StringRef (for API usage)
 | 
			
		||||
  StringRef get_effect_name_ref();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This lets front-end components subscribe to light change events. This callback is called once
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ from ..defines import (
 | 
			
		||||
    literal,
 | 
			
		||||
)
 | 
			
		||||
from ..lv_validation import (
 | 
			
		||||
    lv_angle,
 | 
			
		||||
    lv_angle_degrees,
 | 
			
		||||
    lv_bool,
 | 
			
		||||
    lv_color,
 | 
			
		||||
    lv_image,
 | 
			
		||||
@@ -395,15 +395,15 @@ ARC_PROPS = {
 | 
			
		||||
    DRAW_OPA_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_RADIUS): pixels,
 | 
			
		||||
            cv.Required(CONF_START_ANGLE): lv_angle,
 | 
			
		||||
            cv.Required(CONF_END_ANGLE): lv_angle,
 | 
			
		||||
            cv.Required(CONF_START_ANGLE): lv_angle_degrees,
 | 
			
		||||
            cv.Required(CONF_END_ANGLE): lv_angle_degrees,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}),
 | 
			
		||||
)
 | 
			
		||||
async def canvas_draw_arc(config, action_id, template_arg, args):
 | 
			
		||||
    radius = await size.process(config[CONF_RADIUS])
 | 
			
		||||
    start_angle = await lv_angle.process(config[CONF_START_ANGLE])
 | 
			
		||||
    end_angle = await lv_angle.process(config[CONF_END_ANGLE])
 | 
			
		||||
    start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
 | 
			
		||||
    end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
 | 
			
		||||
 | 
			
		||||
    async def do_draw_arc(w: Widget, x, y, dsc_addr):
 | 
			
		||||
        lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_VALUE,
 | 
			
		||||
    CONF_WIDTH,
 | 
			
		||||
)
 | 
			
		||||
from esphome.cpp_generator import IntLiteral
 | 
			
		||||
 | 
			
		||||
from ..automation import action_to_code
 | 
			
		||||
from ..defines import (
 | 
			
		||||
@@ -32,7 +31,7 @@ from ..helpers import add_lv_use, lvgl_components_required
 | 
			
		||||
from ..lv_validation import (
 | 
			
		||||
    get_end_value,
 | 
			
		||||
    get_start_value,
 | 
			
		||||
    lv_angle,
 | 
			
		||||
    lv_angle_degrees,
 | 
			
		||||
    lv_bool,
 | 
			
		||||
    lv_color,
 | 
			
		||||
    lv_float,
 | 
			
		||||
@@ -163,7 +162,7 @@ SCALE_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
 | 
			
		||||
        cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
 | 
			
		||||
        cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
 | 
			
		||||
        cv.Optional(CONF_ROTATION): lv_angle,
 | 
			
		||||
        cv.Optional(CONF_ROTATION): lv_angle_degrees,
 | 
			
		||||
        cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -188,9 +187,7 @@ class MeterType(WidgetType):
 | 
			
		||||
        for scale_conf in config.get(CONF_SCALES, ()):
 | 
			
		||||
            rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
 | 
			
		||||
            if CONF_ROTATION in scale_conf:
 | 
			
		||||
                rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
 | 
			
		||||
                if isinstance(rotation, IntLiteral):
 | 
			
		||||
                    rotation = int(str(rotation)) // 10
 | 
			
		||||
                rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
 | 
			
		||||
            with LocalVariable(
 | 
			
		||||
                "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
 | 
			
		||||
            ) as meter_var:
 | 
			
		||||
 
 | 
			
		||||
@@ -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<uint8_t>(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<uint8_t>(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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -764,7 +764,8 @@ void Nextion::process_nextion_commands_() {
 | 
			
		||||
        variable_name = to_process.substr(0, index);
 | 
			
		||||
        ++index;
 | 
			
		||||
 | 
			
		||||
        text_value = to_process.substr(index);
 | 
			
		||||
        // Get variable value without terminating NUL byte.  Length check above ensures substr len >= 0.
 | 
			
		||||
        text_value = to_process.substr(index, to_process_length - index - 1);
 | 
			
		||||
 | 
			
		||||
        ESP_LOGN(TAG, "Text sensor: %s='%s'", variable_name.c_str(), text_value.c_str());
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ void OPT3001Sensor::read_lx_(const std::function<void(float)> &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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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_);
 | 
			
		||||
 
 | 
			
		||||
@@ -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_);
 | 
			
		||||
 
 | 
			
		||||
@@ -23,20 +23,18 @@ void Pipsolar::loop() {
 | 
			
		||||
  // Read message
 | 
			
		||||
  if (this->state_ == STATE_IDLE) {
 | 
			
		||||
    this->empty_uart_buffer_();
 | 
			
		||||
    switch (this->send_next_command_()) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        // no command send (empty queue) time to poll
 | 
			
		||||
        if (millis() - this->last_poll_ > this->update_interval_) {
 | 
			
		||||
          this->send_next_poll_();
 | 
			
		||||
          this->last_poll_ = millis();
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
        break;
 | 
			
		||||
      case 1:
 | 
			
		||||
        // command send
 | 
			
		||||
        return;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
    if (this->send_next_command_()) {
 | 
			
		||||
      // command sent
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->send_next_poll_()) {
 | 
			
		||||
      // poll sent
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_ == STATE_COMMAND_COMPLETE) {
 | 
			
		||||
    if (this->check_incoming_length_(4)) {
 | 
			
		||||
@@ -530,7 +528,7 @@ void Pipsolar::loop() {
 | 
			
		||||
        // '(00000000000000000000000000000000'
 | 
			
		||||
        // iterate over all available flag (as not all models have all flags, but at least in the same order)
 | 
			
		||||
        this->value_warnings_present_ = false;
 | 
			
		||||
        this->value_faults_present_ = true;
 | 
			
		||||
        this->value_faults_present_ = false;
 | 
			
		||||
 | 
			
		||||
        for (size_t i = 1; i < strlen(tmp); i++) {
 | 
			
		||||
          enabled = tmp[i] == '1';
 | 
			
		||||
@@ -708,6 +706,7 @@ void Pipsolar::loop() {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      // crc ok
 | 
			
		||||
      this->used_polling_commands_[this->last_polling_command_].needs_update = false;
 | 
			
		||||
      this->state_ = STATE_POLL_CHECKED;
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -788,7 +787,7 @@ uint8_t Pipsolar::check_incoming_crc_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// send next command used
 | 
			
		||||
uint8_t Pipsolar::send_next_command_() {
 | 
			
		||||
bool Pipsolar::send_next_command_() {
 | 
			
		||||
  uint16_t crc16;
 | 
			
		||||
  if (!this->command_queue_[this->command_queue_position_].empty()) {
 | 
			
		||||
    const char *command = this->command_queue_[this->command_queue_position_].c_str();
 | 
			
		||||
@@ -809,37 +808,43 @@ uint8_t Pipsolar::send_next_command_() {
 | 
			
		||||
    // end Byte
 | 
			
		||||
    this->write(0x0D);
 | 
			
		||||
    ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length);
 | 
			
		||||
    return 1;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return 0;
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipsolar::send_next_poll_() {
 | 
			
		||||
bool Pipsolar::send_next_poll_() {
 | 
			
		||||
  uint16_t crc16;
 | 
			
		||||
  this->last_polling_command_ = (this->last_polling_command_ + 1) % 15;
 | 
			
		||||
  if (this->used_polling_commands_[this->last_polling_command_].length == 0) {
 | 
			
		||||
    this->last_polling_command_ = 0;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < POLLING_COMMANDS_MAX; i++) {
 | 
			
		||||
    this->last_polling_command_ = (this->last_polling_command_ + 1) % POLLING_COMMANDS_MAX;
 | 
			
		||||
    if (this->used_polling_commands_[this->last_polling_command_].length == 0) {
 | 
			
		||||
      // not enabled
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if (!this->used_polling_commands_[this->last_polling_command_].needs_update) {
 | 
			
		||||
      // no update requested
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    this->state_ = STATE_POLL;
 | 
			
		||||
    this->command_start_millis_ = millis();
 | 
			
		||||
    this->empty_uart_buffer_();
 | 
			
		||||
    this->read_pos_ = 0;
 | 
			
		||||
    crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
                                this->used_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
    this->write_array(this->used_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
                      this->used_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
    // checksum
 | 
			
		||||
    this->write(((uint8_t) ((crc16) >> 8)));   // highbyte
 | 
			
		||||
    this->write(((uint8_t) ((crc16) &0xff)));  // lowbyte
 | 
			
		||||
    // end Byte
 | 
			
		||||
    this->write(0x0D);
 | 
			
		||||
    ESP_LOGD(TAG, "Sending polling command : %s with length %d",
 | 
			
		||||
             this->used_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
             this->used_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->used_polling_commands_[this->last_polling_command_].length == 0) {
 | 
			
		||||
    // no command specified
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->state_ = STATE_POLL;
 | 
			
		||||
  this->command_start_millis_ = millis();
 | 
			
		||||
  this->empty_uart_buffer_();
 | 
			
		||||
  this->read_pos_ = 0;
 | 
			
		||||
  crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
                              this->used_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
  this->write_array(this->used_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
                    this->used_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
  // checksum
 | 
			
		||||
  this->write(((uint8_t) ((crc16) >> 8)));   // highbyte
 | 
			
		||||
  this->write(((uint8_t) ((crc16) &0xff)));  // lowbyte
 | 
			
		||||
  // end Byte
 | 
			
		||||
  this->write(0x0D);
 | 
			
		||||
  ESP_LOGD(TAG, "Sending polling command : %s with length %d",
 | 
			
		||||
           this->used_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
           this->used_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipsolar::queue_command_(const char *command, uint8_t length) {
 | 
			
		||||
@@ -869,7 +874,13 @@ void Pipsolar::dump_config() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Pipsolar::update() {}
 | 
			
		||||
void Pipsolar::update() {
 | 
			
		||||
  for (auto &used_polling_command : this->used_polling_commands_) {
 | 
			
		||||
    if (used_polling_command.length != 0) {
 | 
			
		||||
      used_polling_command.needs_update = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) {
 | 
			
		||||
  for (auto &used_polling_command : this->used_polling_commands_) {
 | 
			
		||||
@@ -891,6 +902,7 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll
 | 
			
		||||
      used_polling_command.errors = 0;
 | 
			
		||||
      used_polling_command.identifier = polling_command;
 | 
			
		||||
      used_polling_command.length = length - 1;
 | 
			
		||||
      used_polling_command.needs_update = true;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ struct PollingCommand {
 | 
			
		||||
  uint8_t length = 0;
 | 
			
		||||
  uint8_t errors;
 | 
			
		||||
  ENUMPollingCommand identifier;
 | 
			
		||||
  bool needs_update;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
 | 
			
		||||
@@ -189,14 +190,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
  static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110;  // maximum supported answer length
 | 
			
		||||
  static const size_t COMMAND_QUEUE_LENGTH = 10;
 | 
			
		||||
  static const size_t COMMAND_TIMEOUT = 5000;
 | 
			
		||||
  uint32_t last_poll_ = 0;
 | 
			
		||||
  static const size_t POLLING_COMMANDS_MAX = 15;
 | 
			
		||||
  void add_polling_command_(const char *command, ENUMPollingCommand polling_command);
 | 
			
		||||
  void empty_uart_buffer_();
 | 
			
		||||
  uint8_t check_incoming_crc_();
 | 
			
		||||
  uint8_t check_incoming_length_(uint8_t length);
 | 
			
		||||
  uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
 | 
			
		||||
  uint8_t send_next_command_();
 | 
			
		||||
  void send_next_poll_();
 | 
			
		||||
  bool send_next_command_();
 | 
			
		||||
  bool send_next_poll_();
 | 
			
		||||
  void queue_command_(const char *command, uint8_t length);
 | 
			
		||||
  std::string command_queue_[COMMAND_QUEUE_LENGTH];
 | 
			
		||||
  uint8_t command_queue_position_ = 0;
 | 
			
		||||
@@ -216,7 +217,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  uint8_t last_polling_command_ = 0;
 | 
			
		||||
  PollingCommand used_polling_commands_[15];
 | 
			
		||||
  PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace pipsolar
 | 
			
		||||
 
 | 
			
		||||
@@ -46,10 +46,32 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
 | 
			
		||||
      }
 | 
			
		||||
      this->connection_established_ = true;
 | 
			
		||||
      this->char_handle_ = chr->handle;
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
      this->sync_time_();
 | 
			
		||||
#endif
 | 
			
		||||
      this->display();
 | 
			
		||||
 | 
			
		||||
      // Attempt to write immediately
 | 
			
		||||
      // For devices without security, this will work
 | 
			
		||||
      // For devices with security that are already paired, this will work
 | 
			
		||||
      // For devices that need pairing, the write will be retried after auth completes
 | 
			
		||||
      this->sync_time_and_display_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GAP_BLE_AUTH_CMPL_EVT: {
 | 
			
		||||
      if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
      if (param->ble_security.auth_cmpl.success) {
 | 
			
		||||
        ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str());
 | 
			
		||||
        // Now that pairing is complete, perform the pending writes
 | 
			
		||||
        this->sync_time_and_display_();
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str());
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
@@ -127,6 +149,13 @@ void PVVXDisplay::delayed_disconnect_() {
 | 
			
		||||
  this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PVVXDisplay::sync_time_and_display_() {
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  this->sync_time_();
 | 
			
		||||
#endif
 | 
			
		||||
  this->display();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
void PVVXDisplay::sync_time_() {
 | 
			
		||||
  if (this->time_ == nullptr)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
 | 
			
		||||
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
 | 
			
		||||
 | 
			
		||||
  /// Set validity period of the display information in seconds (1..65535)
 | 
			
		||||
  void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; }
 | 
			
		||||
@@ -112,6 +113,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
 | 
			
		||||
  void setcfgbit_(uint8_t bit, bool value);
 | 
			
		||||
  void send_to_setup_char_(uint8_t *blk, size_t size);
 | 
			
		||||
  void delayed_disconnect_();
 | 
			
		||||
  void sync_time_and_display_();
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  void sync_time_();
 | 
			
		||||
  time::RealTimeClock *time_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void()> on_finished_playback_callback_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,10 +53,14 @@ void SenseAirComponent::update() {
 | 
			
		||||
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
  const uint8_t length = response[2];
 | 
			
		||||
  const uint16_t status = (uint16_t(response[3]) << 8) | response[4];
 | 
			
		||||
  const int16_t ppm = int16_t((response[length + 1] << 8) | response[length + 2]);
 | 
			
		||||
  const uint16_t status = encode_uint16(response[3], response[4]);
 | 
			
		||||
  const uint16_t ppm = encode_uint16(response[length + 1], response[length + 2]);
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status);
 | 
			
		||||
  ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status);
 | 
			
		||||
  if (ppm == 0 && (status & SenseAirStatus::OUT_OF_RANGE_ERROR) != 0) {
 | 
			
		||||
    ESP_LOGD(TAG, "Discarding 0 ppm reading with out-of-range status.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->co2_sensor_ != nullptr)
 | 
			
		||||
    this->co2_sensor_->publish_state(ppm);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,17 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace senseair {
 | 
			
		||||
 | 
			
		||||
enum SenseAirStatus : uint8_t {
 | 
			
		||||
  FATAL_ERROR = 1 << 0,
 | 
			
		||||
  OFFSET_ERROR = 1 << 1,
 | 
			
		||||
  ALGORITHM_ERROR = 1 << 2,
 | 
			
		||||
  OUTPUT_ERROR = 1 << 3,
 | 
			
		||||
  SELF_DIAGNOSTIC_ERROR = 1 << 4,
 | 
			
		||||
  OUT_OF_RANGE_ERROR = 1 << 5,
 | 
			
		||||
  MEMORY_ERROR = 1 << 6,
 | 
			
		||||
  RESERVED = 1 << 7
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SenseAirComponent : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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_;
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -52,9 +52,9 @@ def default_url(config: ConfigType) -> ConfigType:
 | 
			
		||||
    config = config.copy()
 | 
			
		||||
    if config[CONF_VERSION] == 1:
 | 
			
		||||
        if CONF_CSS_URL not in config:
 | 
			
		||||
            config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css"
 | 
			
		||||
            config[CONF_CSS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.css"
 | 
			
		||||
        if CONF_JS_URL not in config:
 | 
			
		||||
            config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js"
 | 
			
		||||
            config[CONF_JS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.js"
 | 
			
		||||
    if config[CONF_VERSION] == 2:
 | 
			
		||||
        if CONF_CSS_URL not in config:
 | 
			
		||||
            config[CONF_CSS_URL] = ""
 | 
			
		||||
 
 | 
			
		||||
@@ -813,7 +813,7 @@ std::string WebServer::cover_state_json_generator(WebServer *web_server, void *s
 | 
			
		||||
  return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE);
 | 
			
		||||
}
 | 
			
		||||
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
 | 
			
		||||
  return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE);
 | 
			
		||||
  return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL);
 | 
			
		||||
}
 | 
			
		||||
std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
 | 
			
		||||
  return json::build_json([this, obj, start_config](JsonObject root) {
 | 
			
		||||
 
 | 
			
		||||
@@ -173,14 +173,14 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
 | 
			
		||||
 | 
			
		||||
#if USE_WEBSERVER_VERSION == 1
 | 
			
		||||
  /** Set the URL to the CSS <link> that's sent to each client. Defaults to
 | 
			
		||||
   * https://esphome.io/_static/webserver-v1.min.css
 | 
			
		||||
   * https://oi.esphome.io/v1/webserver-v1.min.css
 | 
			
		||||
   *
 | 
			
		||||
   * @param css_url The url to the web server stylesheet.
 | 
			
		||||
   */
 | 
			
		||||
  void set_css_url(const char *css_url);
 | 
			
		||||
 | 
			
		||||
  /** Set the URL to the script that's embedded in the index page. Defaults to
 | 
			
		||||
   * https://esphome.io/_static/webserver-v1.min.js
 | 
			
		||||
   * https://oi.esphome.io/v1/webserver-v1.min.js
 | 
			
		||||
   *
 | 
			
		||||
   * @param js_url The url to the web server script.
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -253,7 +253,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
 | 
			
		||||
  esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out,
 | 
			
		||||
                           reinterpret_cast<const uint8_t *>(user_info.c_str()), user_info.size());
 | 
			
		||||
 | 
			
		||||
  return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0;
 | 
			
		||||
  return strcmp(digest.get(), auth_str + auth_prefix_len) == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
 | 
			
		||||
 
 | 
			
		||||
@@ -375,11 +375,16 @@ async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
 | 
			
		||||
 | 
			
		||||
    # Track if any network uses Enterprise authentication
 | 
			
		||||
    has_eap = False
 | 
			
		||||
 | 
			
		||||
    def add_sta(ap, network):
 | 
			
		||||
        ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
 | 
			
		||||
        cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
 | 
			
		||||
 | 
			
		||||
    for network in config.get(CONF_NETWORKS, []):
 | 
			
		||||
        if CONF_EAP in network:
 | 
			
		||||
            has_eap = True
 | 
			
		||||
        cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
 | 
			
		||||
 | 
			
		||||
    if CONF_AP in config:
 | 
			
		||||
@@ -396,6 +401,10 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
 | 
			
		||||
 | 
			
		||||
    # Disable Enterprise WiFi support if no EAP is configured
 | 
			
		||||
    if CORE.is_esp32 and CORE.using_esp_idf and not has_eap:
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
 | 
			
		||||
    cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
 | 
			
		||||
    cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from enum import Enum
 | 
			
		||||
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.8.0b2"
 | 
			
		||||
__version__ = "2025.8.2"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
 
 | 
			
		||||
@@ -803,6 +803,10 @@ class EsphomeCore:
 | 
			
		||||
            raise TypeError(
 | 
			
		||||
                f"Library {library} must be instance of Library, not {type(library)}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if not library.name:
 | 
			
		||||
            raise ValueError(f"The library for {library.repository} must have a name")
 | 
			
		||||
 | 
			
		||||
        short_name = (
 | 
			
		||||
            library.name if "/" not in library.name else library.name.split("/")[-1]
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/core/scheduler.h"
 | 
			
		||||
#include "esphome/core/string_ref.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
#include "esphome/core/device.h"
 | 
			
		||||
@@ -248,6 +249,8 @@ class Application {
 | 
			
		||||
  bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
 | 
			
		||||
 | 
			
		||||
  std::string get_compilation_time() const { return this->compilation_time_; }
 | 
			
		||||
  /// Get the compilation time as StringRef (for API usage)
 | 
			
		||||
  StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); }
 | 
			
		||||
 | 
			
		||||
  /// Get the cached time in milliseconds from when the current component started its loop execution
 | 
			
		||||
  inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/core/scheduler.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
@@ -158,7 +160,16 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
 | 
			
		||||
  void play_complex(Ts... x) override {
 | 
			
		||||
    auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
 | 
			
		||||
    this->num_running_++;
 | 
			
		||||
    this->set_timeout("delay", this->delay_.value(x...), f);
 | 
			
		||||
 | 
			
		||||
    // If num_running_ > 1, we have multiple instances running in parallel
 | 
			
		||||
    // In single/restart/queued modes, only one instance runs at a time
 | 
			
		||||
    // Parallel mode uses skip_cancel=true to allow multiple delays to coexist
 | 
			
		||||
    // WARNING: This can accumulate delays if scripts are triggered faster than they complete!
 | 
			
		||||
    // Users should set max_runs on parallel scripts to limit concurrent executions.
 | 
			
		||||
    // Issue #10264: This is a workaround for parallel script delays interfering with each other.
 | 
			
		||||
    App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT,
 | 
			
		||||
                                    /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f),
 | 
			
		||||
                                    /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
 | 
			
		||||
  }
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -236,10 +236,21 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
 | 
			
		||||
            if existing_component != "unknown":
 | 
			
		||||
                conflict_msg += f" from component '{existing_component}'"
 | 
			
		||||
 | 
			
		||||
            # Show both original names and their ASCII-only versions if they differ
 | 
			
		||||
            sanitized_msg = ""
 | 
			
		||||
            if entity_name != existing_name:
 | 
			
		||||
                sanitized_msg = (
 | 
			
		||||
                    f"\n  Original names: '{entity_name}' and '{existing_name}'"
 | 
			
		||||
                    f"\n  Both convert to ASCII ID: '{name_key}'"
 | 
			
		||||
                    "\n  To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')"
 | 
			
		||||
                    "\n          to distinguish them"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
 | 
			
		||||
                f"{conflict_msg}. "
 | 
			
		||||
                f"Each entity on a device must have a unique name within its platform."
 | 
			
		||||
                "Each entity on a device must have a unique name within its platform."
 | 
			
		||||
                f"{sanitized_msg}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # Store metadata about this entity
 | 
			
		||||
 
 | 
			
		||||
@@ -65,14 +65,17 @@ static void validate_static_string(const char *name) {
 | 
			
		||||
 | 
			
		||||
// Common implementation for both timeout and interval
 | 
			
		||||
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
 | 
			
		||||
                                      const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry) {
 | 
			
		||||
                                      const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry,
 | 
			
		||||
                                      bool skip_cancel) {
 | 
			
		||||
  // Get the name as const char*
 | 
			
		||||
  const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
 | 
			
		||||
 | 
			
		||||
  if (delay == SCHEDULER_DONT_RUN) {
 | 
			
		||||
    // Still need to cancel existing timer if name is not empty
 | 
			
		||||
    LockGuard guard{this->lock_};
 | 
			
		||||
    this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
    if (!skip_cancel) {
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
      this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -82,7 +85,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
 | 
			
		||||
  item->set_name(name_cstr, !is_static_string);
 | 
			
		||||
  item->type = type;
 | 
			
		||||
  item->callback = std::move(func);
 | 
			
		||||
  // Initialize remove to false (though it should already be from constructor)
 | 
			
		||||
  // Not using mark_item_removed_ helper since we're setting to false, not true
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
 | 
			
		||||
  item->remove.store(false, std::memory_order_relaxed);
 | 
			
		||||
#else
 | 
			
		||||
  item->remove = false;
 | 
			
		||||
#endif
 | 
			
		||||
  item->is_retry = is_retry;
 | 
			
		||||
 | 
			
		||||
#ifndef ESPHOME_THREAD_SINGLE
 | 
			
		||||
@@ -91,7 +100,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
 | 
			
		||||
  if (delay == 0 && type == SchedulerItem::TIMEOUT) {
 | 
			
		||||
    // Put in defer queue for guaranteed FIFO execution
 | 
			
		||||
    LockGuard guard{this->lock_};
 | 
			
		||||
    this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
    if (!skip_cancel) {
 | 
			
		||||
      this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
    }
 | 
			
		||||
    this->defer_queue_.push_back(std::move(item));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -144,9 +155,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If name is provided, do atomic cancel-and-add
 | 
			
		||||
  // If name is provided, do atomic cancel-and-add (unless skip_cancel is true)
 | 
			
		||||
  // Cancel existing items
 | 
			
		||||
  this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
  if (!skip_cancel) {
 | 
			
		||||
    this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
  }
 | 
			
		||||
  // Add new item directly to to_add_
 | 
			
		||||
  // since we have the lock held
 | 
			
		||||
  this->to_add_.push_back(std::move(item));
 | 
			
		||||
@@ -398,6 +411,31 @@ void HOT Scheduler::call(uint32_t now) {
 | 
			
		||||
        this->pop_raw_();
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Check if item is marked for removal
 | 
			
		||||
      // This handles two cases:
 | 
			
		||||
      // 1. Item was marked for removal after cleanup_() but before we got here
 | 
			
		||||
      // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_()
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
 | 
			
		||||
      // Multi-threaded platforms without atomics: must take lock to safely read remove flag
 | 
			
		||||
      {
 | 
			
		||||
        LockGuard guard{this->lock_};
 | 
			
		||||
        if (is_item_removed_(item.get())) {
 | 
			
		||||
          this->pop_raw_();
 | 
			
		||||
          this->to_remove_--;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
#else
 | 
			
		||||
      // Single-threaded or multi-threaded with atomics: can check without lock
 | 
			
		||||
      if (is_item_removed_(item.get())) {
 | 
			
		||||
        LockGuard guard{this->lock_};
 | 
			
		||||
        this->pop_raw_();
 | 
			
		||||
        this->to_remove_--;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ESPHOME_DEBUG_SCHEDULER
 | 
			
		||||
      const char *item_name = item->get_name();
 | 
			
		||||
      ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
 | 
			
		||||
@@ -518,7 +556,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
 | 
			
		||||
  if (type == SchedulerItem::TIMEOUT) {
 | 
			
		||||
    for (auto &item : this->defer_queue_) {
 | 
			
		||||
      if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
 | 
			
		||||
        item->remove = true;
 | 
			
		||||
        this->mark_item_removed_(item.get());
 | 
			
		||||
        total_cancelled++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -528,7 +566,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
 | 
			
		||||
  // Cancel items in the main heap
 | 
			
		||||
  for (auto &item : this->items_) {
 | 
			
		||||
    if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
 | 
			
		||||
      item->remove = true;
 | 
			
		||||
      this->mark_item_removed_(item.get());
 | 
			
		||||
      total_cancelled++;
 | 
			
		||||
      this->to_remove_++;  // Track removals for heap items
 | 
			
		||||
    }
 | 
			
		||||
@@ -537,7 +575,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
 | 
			
		||||
  // Cancel items in to_add_
 | 
			
		||||
  for (auto &item : this->to_add_) {
 | 
			
		||||
    if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
 | 
			
		||||
      item->remove = true;
 | 
			
		||||
      this->mark_item_removed_(item.get());
 | 
			
		||||
      total_cancelled++;
 | 
			
		||||
      // Don't track removals for to_add_ items
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,13 @@ struct RetryArgs;
 | 
			
		||||
void retry_handler(const std::shared_ptr<RetryArgs> &args);
 | 
			
		||||
 | 
			
		||||
class Scheduler {
 | 
			
		||||
  // Allow retry_handler to access protected members
 | 
			
		||||
  // Allow retry_handler to access protected members for internal retry mechanism
 | 
			
		||||
  friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
 | 
			
		||||
  // Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
 | 
			
		||||
  // This is needed to fix issue #10264 where parallel scripts with delays interfere with each other.
 | 
			
		||||
  // We use friend instead of a public API because skip_cancel is dangerous - it can cause delays
 | 
			
		||||
  // to accumulate and overload the scheduler if misused.
 | 
			
		||||
  template<typename... Ts> friend class DelayAction;
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  // Public API - accepts std::string for backward compatibility
 | 
			
		||||
@@ -97,22 +102,42 @@ class Scheduler {
 | 
			
		||||
 | 
			
		||||
    std::function<void()> callback;
 | 
			
		||||
 | 
			
		||||
    // Bit-packed fields to minimize padding
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
 | 
			
		||||
    // Multi-threaded with atomics: use atomic for lock-free access
 | 
			
		||||
    // Place atomic<bool> separately since it can't be packed with bit fields
 | 
			
		||||
    std::atomic<bool> remove{false};
 | 
			
		||||
 | 
			
		||||
    // Bit-packed fields (3 bits used, 5 bits padding in 1 byte)
 | 
			
		||||
    enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
 | 
			
		||||
    bool name_is_dynamic : 1;  // True if name was dynamically allocated (needs delete[])
 | 
			
		||||
    bool is_retry : 1;         // True if this is a retry timeout
 | 
			
		||||
                               // 5 bits padding
 | 
			
		||||
#else
 | 
			
		||||
    // Single-threaded or multi-threaded without atomics: can pack all fields together
 | 
			
		||||
    // Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
 | 
			
		||||
    enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
 | 
			
		||||
    bool remove : 1;
 | 
			
		||||
    bool name_is_dynamic : 1;  // True if name was dynamically allocated (needs delete[])
 | 
			
		||||
    bool is_retry : 1;         // True if this is a retry timeout
 | 
			
		||||
    // 4 bits padding
 | 
			
		||||
                               // 4 bits padding
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    // Constructor
 | 
			
		||||
    SchedulerItem()
 | 
			
		||||
        : component(nullptr),
 | 
			
		||||
          interval(0),
 | 
			
		||||
          next_execution_(0),
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
 | 
			
		||||
          // remove is initialized in the member declaration as std::atomic<bool>{false}
 | 
			
		||||
          type(TIMEOUT),
 | 
			
		||||
          name_is_dynamic(false),
 | 
			
		||||
          is_retry(false) {
 | 
			
		||||
#else
 | 
			
		||||
          type(TIMEOUT),
 | 
			
		||||
          remove(false),
 | 
			
		||||
          name_is_dynamic(false),
 | 
			
		||||
          is_retry(false) {
 | 
			
		||||
#endif
 | 
			
		||||
      name_.static_name = nullptr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -164,7 +189,7 @@ class Scheduler {
 | 
			
		||||
 | 
			
		||||
  // Common implementation for both timeout and interval
 | 
			
		||||
  void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
 | 
			
		||||
                         uint32_t delay, std::function<void()> func, bool is_retry = false);
 | 
			
		||||
                         uint32_t delay, std::function<void()> func, bool is_retry = false, bool skip_cancel = false);
 | 
			
		||||
 | 
			
		||||
  // Common implementation for retry
 | 
			
		||||
  void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
 | 
			
		||||
@@ -219,6 +244,37 @@ class Scheduler {
 | 
			
		||||
    return item->remove || (item->component != nullptr && item->component->is_failed());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper to check if item is marked for removal (platform-specific)
 | 
			
		||||
  // Returns true if item should be skipped, handles platform-specific synchronization
 | 
			
		||||
  // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
 | 
			
		||||
  // function.
 | 
			
		||||
  bool is_item_removed_(SchedulerItem *item) const {
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
 | 
			
		||||
    // Multi-threaded with atomics: use atomic load for lock-free access
 | 
			
		||||
    return item->remove.load(std::memory_order_acquire);
 | 
			
		||||
#else
 | 
			
		||||
    // Single-threaded (ESPHOME_THREAD_SINGLE) or
 | 
			
		||||
    // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read
 | 
			
		||||
    // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
 | 
			
		||||
    return item->remove;
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper to mark item for removal (platform-specific)
 | 
			
		||||
  // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
 | 
			
		||||
  // function.
 | 
			
		||||
  void mark_item_removed_(SchedulerItem *item) {
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
 | 
			
		||||
    // Multi-threaded with atomics: use atomic store
 | 
			
		||||
    item->remove.store(true, std::memory_order_release);
 | 
			
		||||
#else
 | 
			
		||||
    // Single-threaded (ESPHOME_THREAD_SINGLE) or
 | 
			
		||||
    // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
 | 
			
		||||
    // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
 | 
			
		||||
    item->remove = true;
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Template helper to check if any item in a container matches our criteria
 | 
			
		||||
  template<typename Container>
 | 
			
		||||
  bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
 | 
			
		||||
 
 | 
			
		||||
@@ -80,13 +80,16 @@ def replace_file_content(text, pattern, repl):
 | 
			
		||||
    return content_new, count
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
 | 
			
		||||
def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
 | 
			
		||||
    if old is None:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    if old.src_version != new.src_version:
 | 
			
		||||
        return True
 | 
			
		||||
    return old.build_path != new.build_path
 | 
			
		||||
    if old.build_path != new.build_path:
 | 
			
		||||
        return True
 | 
			
		||||
    # Check if any components have been removed
 | 
			
		||||
    return bool(old.loaded_integrations - new.loaded_integrations)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
 | 
			
		||||
@@ -100,7 +103,7 @@ def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> boo
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_storage_json():
 | 
			
		||||
def update_storage_json() -> None:
 | 
			
		||||
    path = storage_path()
 | 
			
		||||
    old = StorageJSON.load(path)
 | 
			
		||||
    new = StorageJSON.from_esphome_core(CORE, old)
 | 
			
		||||
@@ -108,7 +111,14 @@ def update_storage_json():
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if storage_should_clean(old, new):
 | 
			
		||||
        _LOGGER.info("Core config, version changed, cleaning build files...")
 | 
			
		||||
        if old is not None and old.loaded_integrations - new.loaded_integrations:
 | 
			
		||||
            removed = old.loaded_integrations - new.loaded_integrations
 | 
			
		||||
            _LOGGER.info(
 | 
			
		||||
                "Components removed (%s), cleaning build files...",
 | 
			
		||||
                ", ".join(sorted(removed)),
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            _LOGGER.info("Core config or version changed, cleaning build files...")
 | 
			
		||||
        clean_build()
 | 
			
		||||
    elif storage_should_update_cmake_cache(old, new):
 | 
			
		||||
        _LOGGER.info("Integrations changed, cleaning cmake cache...")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								tests/components/deep_sleep/test.esp32-c6-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/deep_sleep/test.esp32-c6-idf.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
substitutions:
 | 
			
		||||
  wakeup_pin: GPIO4
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
<<: !include common-esp32.yaml
 | 
			
		||||
							
								
								
									
										5
									
								
								tests/components/deep_sleep/test.esp32-s2-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/deep_sleep/test.esp32-s2-idf.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
substitutions:
 | 
			
		||||
  wakeup_pin: GPIO4
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
<<: !include common-esp32.yaml
 | 
			
		||||
							
								
								
									
										5
									
								
								tests/components/deep_sleep/test.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/deep_sleep/test.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
substitutions:
 | 
			
		||||
  wakeup_pin: GPIO4
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
<<: !include common-esp32.yaml
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
esp32_ble_tracker:
 | 
			
		||||
  on_scan_end:
 | 
			
		||||
    - logger.log: "Scan ended!"
 | 
			
		||||
@@ -56,10 +56,29 @@ light:
 | 
			
		||||
    warm_white_color_temperature: 2000 K
 | 
			
		||||
    constant_brightness: true
 | 
			
		||||
    effects:
 | 
			
		||||
      # Use default parameters:
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "Random Effect"
 | 
			
		||||
      # Customize parameters - use longer names to potentially trigger buffer issues
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "My Very Slow Random Effect With Long Name"
 | 
			
		||||
          transition_length: 30ms
 | 
			
		||||
          update_interval: 30ms
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "My Fast Random Effect That Changes Quickly"
 | 
			
		||||
          transition_length: 4ms
 | 
			
		||||
          update_interval: 5ms
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "Random Effect With Medium Length Name Here"
 | 
			
		||||
          transition_length: 100ms
 | 
			
		||||
          update_interval: 200ms
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "Another Random Effect With Different Parameters"
 | 
			
		||||
          transition_length: 2ms
 | 
			
		||||
          update_interval: 3ms
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "Yet Another Random Effect To Test Memory"
 | 
			
		||||
          transition_length: 15ms
 | 
			
		||||
          update_interval: 20ms
 | 
			
		||||
      - strobe:
 | 
			
		||||
          name: "Strobe Effect"
 | 
			
		||||
      - pulse:
 | 
			
		||||
@@ -73,6 +92,17 @@ light:
 | 
			
		||||
    red: test_red
 | 
			
		||||
    green: test_green
 | 
			
		||||
    blue: test_blue
 | 
			
		||||
    effects:
 | 
			
		||||
      # Same random effects to test for cross-contamination
 | 
			
		||||
      - random:
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "RGB Slow Random"
 | 
			
		||||
          transition_length: 20ms
 | 
			
		||||
          update_interval: 25ms
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "RGB Fast Random"
 | 
			
		||||
          transition_length: 2ms
 | 
			
		||||
          update_interval: 3ms
 | 
			
		||||
 | 
			
		||||
  - platform: binary
 | 
			
		||||
    name: "Test Binary Light"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								tests/integration/fixtures/parallel_script_delays.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								tests/integration/fixtures/parallel_script_delays.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: test-parallel-delays
 | 
			
		||||
 | 
			
		||||
host:
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
  level: DEBUG
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  actions:
 | 
			
		||||
    - action: test_parallel_delays
 | 
			
		||||
      then:
 | 
			
		||||
        # Start three parallel script instances with small delays between starts
 | 
			
		||||
        - globals.set:
 | 
			
		||||
            id: instance_counter
 | 
			
		||||
            value: '1'
 | 
			
		||||
        - script.execute: parallel_delay_script
 | 
			
		||||
        - delay: 10ms
 | 
			
		||||
        - globals.set:
 | 
			
		||||
            id: instance_counter
 | 
			
		||||
            value: '2'
 | 
			
		||||
        - script.execute: parallel_delay_script
 | 
			
		||||
        - delay: 10ms
 | 
			
		||||
        - globals.set:
 | 
			
		||||
            id: instance_counter
 | 
			
		||||
            value: '3'
 | 
			
		||||
        - script.execute: parallel_delay_script
 | 
			
		||||
 | 
			
		||||
globals:
 | 
			
		||||
  - id: instance_counter
 | 
			
		||||
    type: int
 | 
			
		||||
    initial_value: '0'
 | 
			
		||||
 | 
			
		||||
script:
 | 
			
		||||
  - id: parallel_delay_script
 | 
			
		||||
    mode: parallel
 | 
			
		||||
    then:
 | 
			
		||||
      - lambda: !lambda |-
 | 
			
		||||
          int instance = id(instance_counter);
 | 
			
		||||
          ESP_LOGI("TEST", "Parallel script instance %d started", instance);
 | 
			
		||||
      - delay: 1s
 | 
			
		||||
      - lambda: !lambda |-
 | 
			
		||||
          static int completed_counter = 0;
 | 
			
		||||
          completed_counter++;
 | 
			
		||||
          ESP_LOGI("TEST", "Parallel script instance %d completed after delay", completed_counter);
 | 
			
		||||
							
								
								
									
										139
									
								
								tests/integration/fixtures/scheduler_removed_item_race.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								tests/integration/fixtures/scheduler_removed_item_race.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: scheduler-removed-item-race
 | 
			
		||||
 | 
			
		||||
host:
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  services:
 | 
			
		||||
    - service: run_test
 | 
			
		||||
      then:
 | 
			
		||||
        - script.execute: run_test_script
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
  level: DEBUG
 | 
			
		||||
 | 
			
		||||
globals:
 | 
			
		||||
  - id: test_passed
 | 
			
		||||
    type: bool
 | 
			
		||||
    initial_value: 'true'
 | 
			
		||||
  - id: removed_item_executed
 | 
			
		||||
    type: int
 | 
			
		||||
    initial_value: '0'
 | 
			
		||||
  - id: normal_item_executed
 | 
			
		||||
    type: int
 | 
			
		||||
    initial_value: '0'
 | 
			
		||||
 | 
			
		||||
sensor:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_sensor
 | 
			
		||||
    name: "Test Sensor"
 | 
			
		||||
    update_interval: never
 | 
			
		||||
    lambda: return 0.0;
 | 
			
		||||
 | 
			
		||||
script:
 | 
			
		||||
  - id: run_test_script
 | 
			
		||||
    then:
 | 
			
		||||
      - logger.log: "=== Starting Removed Item Race Test ==="
 | 
			
		||||
 | 
			
		||||
      # This test creates a scenario where:
 | 
			
		||||
      # 1. First item in heap is NOT cancelled (cleanup stops immediately)
 | 
			
		||||
      # 2. Items behind it ARE cancelled (remain in heap after cleanup)
 | 
			
		||||
      # 3. All items execute at the same time, including cancelled ones
 | 
			
		||||
 | 
			
		||||
      - lambda: |-
 | 
			
		||||
          // The key to hitting the race:
 | 
			
		||||
          // 1. Add items in a specific order to control heap structure
 | 
			
		||||
          // 2. Cancel ONLY items that won't be at the front
 | 
			
		||||
          // 3. Ensure the first item stays non-cancelled so cleanup_() stops immediately
 | 
			
		||||
 | 
			
		||||
          // Schedule all items to execute at the SAME time (1ms from now)
 | 
			
		||||
          // Using 1ms instead of 0 to avoid defer queue on multi-core platforms
 | 
			
		||||
          // This ensures they'll all be ready together and go through the heap
 | 
			
		||||
          const uint32_t exec_time = 1;
 | 
			
		||||
 | 
			
		||||
          // CRITICAL: Add a non-cancellable item FIRST
 | 
			
		||||
          // This will be at the front of the heap and block cleanup_()
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "blocker", exec_time, []() {
 | 
			
		||||
            ESP_LOGD("test", "Blocker timeout executed (expected) - was at front of heap");
 | 
			
		||||
            id(normal_item_executed)++;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // Now add items that we WILL cancel
 | 
			
		||||
          // These will be behind the blocker in the heap
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "cancel_1", exec_time, []() {
 | 
			
		||||
            ESP_LOGE("test", "RACE: Cancelled timeout 1 executed after being cancelled!");
 | 
			
		||||
            id(removed_item_executed)++;
 | 
			
		||||
            id(test_passed) = false;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "cancel_2", exec_time, []() {
 | 
			
		||||
            ESP_LOGE("test", "RACE: Cancelled timeout 2 executed after being cancelled!");
 | 
			
		||||
            id(removed_item_executed)++;
 | 
			
		||||
            id(test_passed) = false;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "cancel_3", exec_time, []() {
 | 
			
		||||
            ESP_LOGE("test", "RACE: Cancelled timeout 3 executed after being cancelled!");
 | 
			
		||||
            id(removed_item_executed)++;
 | 
			
		||||
            id(test_passed) = false;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // Add some more normal items
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "normal_1", exec_time, []() {
 | 
			
		||||
            ESP_LOGD("test", "Normal timeout 1 executed (expected)");
 | 
			
		||||
            id(normal_item_executed)++;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "normal_2", exec_time, []() {
 | 
			
		||||
            ESP_LOGD("test", "Normal timeout 2 executed (expected)");
 | 
			
		||||
            id(normal_item_executed)++;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          App.scheduler.set_timeout(id(test_sensor), "normal_3", exec_time, []() {
 | 
			
		||||
            ESP_LOGD("test", "Normal timeout 3 executed (expected)");
 | 
			
		||||
            id(normal_item_executed)++;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // Force items into the heap before cancelling
 | 
			
		||||
          App.scheduler.process_to_add();
 | 
			
		||||
 | 
			
		||||
          // NOW cancel the items - they're behind "blocker" in the heap
 | 
			
		||||
          // When cleanup_() runs, it will see "blocker" (not removed) at the front
 | 
			
		||||
          // and stop immediately, leaving cancel_1, cancel_2, cancel_3 in the heap
 | 
			
		||||
          bool c1 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_1");
 | 
			
		||||
          bool c2 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_2");
 | 
			
		||||
          bool c3 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_3");
 | 
			
		||||
 | 
			
		||||
          ESP_LOGD("test", "Cancelled items (behind blocker): %s, %s, %s",
 | 
			
		||||
                   c1 ? "true" : "false",
 | 
			
		||||
                   c2 ? "true" : "false",
 | 
			
		||||
                   c3 ? "true" : "false");
 | 
			
		||||
 | 
			
		||||
          // The heap now has:
 | 
			
		||||
          // - "blocker" at front (not cancelled)
 | 
			
		||||
          // - cancelled items behind it (marked remove=true but still in heap)
 | 
			
		||||
          // - When all execute at once, cleanup_() stops at "blocker"
 | 
			
		||||
          // - The loop then executes ALL ready items including cancelled ones
 | 
			
		||||
 | 
			
		||||
          ESP_LOGD("test", "Setup complete. Blocker at front prevents cleanup of cancelled items behind it");
 | 
			
		||||
 | 
			
		||||
      # Wait for all timeouts to execute (or not)
 | 
			
		||||
      - delay: 20ms
 | 
			
		||||
 | 
			
		||||
      # Check results
 | 
			
		||||
      - lambda: |-
 | 
			
		||||
          ESP_LOGI("test", "=== Test Results ===");
 | 
			
		||||
          ESP_LOGI("test", "Normal items executed: %d (expected 4)", id(normal_item_executed));
 | 
			
		||||
          ESP_LOGI("test", "Removed items executed: %d (expected 0)", id(removed_item_executed));
 | 
			
		||||
 | 
			
		||||
          if (id(removed_item_executed) > 0) {
 | 
			
		||||
            ESP_LOGE("test", "TEST FAILED: %d cancelled items were executed!", id(removed_item_executed));
 | 
			
		||||
            id(test_passed) = false;
 | 
			
		||||
          } else if (id(normal_item_executed) != 4) {
 | 
			
		||||
            ESP_LOGE("test", "TEST FAILED: Expected 4 normal items, got %d", id(normal_item_executed));
 | 
			
		||||
            id(test_passed) = false;
 | 
			
		||||
          } else {
 | 
			
		||||
            ESP_LOGI("test", "TEST PASSED: No cancelled items were executed");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ESP_LOGI("test", "=== Test Complete ===");
 | 
			
		||||
@@ -89,3 +89,73 @@ async def test_delay_action_cancellation(
 | 
			
		||||
        assert 0.4 < time_from_second_start < 0.6, (
 | 
			
		||||
            f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_parallel_script_delays(
 | 
			
		||||
    yaml_config: str,
 | 
			
		||||
    run_compiled: RunCompiledFunction,
 | 
			
		||||
    api_client_connected: APIClientConnectedFactory,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that parallel scripts with delays don't interfere with each other."""
 | 
			
		||||
    loop = asyncio.get_running_loop()
 | 
			
		||||
 | 
			
		||||
    # Track script executions
 | 
			
		||||
    script_starts: list[float] = []
 | 
			
		||||
    script_ends: list[float] = []
 | 
			
		||||
 | 
			
		||||
    # Patterns to match
 | 
			
		||||
    start_pattern = re.compile(r"Parallel script instance \d+ started")
 | 
			
		||||
    end_pattern = re.compile(r"Parallel script instance \d+ completed after delay")
 | 
			
		||||
 | 
			
		||||
    # Future to track when all scripts have completed
 | 
			
		||||
    all_scripts_completed = loop.create_future()
 | 
			
		||||
 | 
			
		||||
    def check_output(line: str) -> None:
 | 
			
		||||
        """Check log output for parallel script messages."""
 | 
			
		||||
        current_time = loop.time()
 | 
			
		||||
 | 
			
		||||
        if start_pattern.search(line):
 | 
			
		||||
            script_starts.append(current_time)
 | 
			
		||||
 | 
			
		||||
        if end_pattern.search(line):
 | 
			
		||||
            script_ends.append(current_time)
 | 
			
		||||
            # Check if we have all 3 completions
 | 
			
		||||
            if len(script_ends) == 3 and not all_scripts_completed.done():
 | 
			
		||||
                all_scripts_completed.set_result(True)
 | 
			
		||||
 | 
			
		||||
    async with (
 | 
			
		||||
        run_compiled(yaml_config, line_callback=check_output),
 | 
			
		||||
        api_client_connected() as client,
 | 
			
		||||
    ):
 | 
			
		||||
        # Get services
 | 
			
		||||
        entities, services = await client.list_entities_services()
 | 
			
		||||
 | 
			
		||||
        # Find our test service
 | 
			
		||||
        test_service = next(
 | 
			
		||||
            (s for s in services if s.name == "test_parallel_delays"), None
 | 
			
		||||
        )
 | 
			
		||||
        assert test_service is not None, "test_parallel_delays service not found"
 | 
			
		||||
 | 
			
		||||
        # Execute the test - this will start 3 parallel scripts with 1 second delays
 | 
			
		||||
        client.execute_service(test_service, {})
 | 
			
		||||
 | 
			
		||||
        # Wait for all scripts to complete (should take ~1 second, not 3)
 | 
			
		||||
        await asyncio.wait_for(all_scripts_completed, timeout=2.0)
 | 
			
		||||
 | 
			
		||||
        # Verify we had 3 starts and 3 ends
 | 
			
		||||
        assert len(script_starts) == 3, (
 | 
			
		||||
            f"Expected 3 script starts, got {len(script_starts)}"
 | 
			
		||||
        )
 | 
			
		||||
        assert len(script_ends) == 3, f"Expected 3 script ends, got {len(script_ends)}"
 | 
			
		||||
 | 
			
		||||
        # Verify they ran in parallel - all should complete within ~1.5 seconds
 | 
			
		||||
        first_start = min(script_starts)
 | 
			
		||||
        last_end = max(script_ends)
 | 
			
		||||
        total_time = last_end - first_start
 | 
			
		||||
 | 
			
		||||
        # If running in parallel, total time should be close to 1 second
 | 
			
		||||
        # If they were interfering (running sequentially), it would take 3+ seconds
 | 
			
		||||
        assert total_time < 1.5, (
 | 
			
		||||
            f"Parallel scripts took {total_time:.2f}s total, should be ~1s if running in parallel"
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -108,14 +108,51 @@ async def test_light_calls(
 | 
			
		||||
        # Wait for flash to end
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
 | 
			
		||||
        # Test 13: effect only
 | 
			
		||||
        # Test 13: effect only - test all random effects
 | 
			
		||||
        # First ensure light is on
 | 
			
		||||
        client.light_command(key=rgbcw_light.key, state=True)
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        # Now set effect
 | 
			
		||||
        client.light_command(key=rgbcw_light.key, effect="Random Effect")
 | 
			
		||||
 | 
			
		||||
        # Test 13a: Default random effect (no name, gets default name "Random")
 | 
			
		||||
        client.light_command(key=rgbcw_light.key, effect="Random")
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        assert state.effect == "Random Effect"
 | 
			
		||||
        assert state.effect == "Random"
 | 
			
		||||
 | 
			
		||||
        # Test 13b: Slow random effect with long name
 | 
			
		||||
        client.light_command(
 | 
			
		||||
            key=rgbcw_light.key, effect="My Very Slow Random Effect With Long Name"
 | 
			
		||||
        )
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        assert state.effect == "My Very Slow Random Effect With Long Name"
 | 
			
		||||
 | 
			
		||||
        # Test 13c: Fast random effect with long name
 | 
			
		||||
        client.light_command(
 | 
			
		||||
            key=rgbcw_light.key, effect="My Fast Random Effect That Changes Quickly"
 | 
			
		||||
        )
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        assert state.effect == "My Fast Random Effect That Changes Quickly"
 | 
			
		||||
 | 
			
		||||
        # Test 13d: Random effect with medium length name
 | 
			
		||||
        client.light_command(
 | 
			
		||||
            key=rgbcw_light.key, effect="Random Effect With Medium Length Name Here"
 | 
			
		||||
        )
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        assert state.effect == "Random Effect With Medium Length Name Here"
 | 
			
		||||
 | 
			
		||||
        # Test 13e: Another random effect
 | 
			
		||||
        client.light_command(
 | 
			
		||||
            key=rgbcw_light.key,
 | 
			
		||||
            effect="Another Random Effect With Different Parameters",
 | 
			
		||||
        )
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        assert state.effect == "Another Random Effect With Different Parameters"
 | 
			
		||||
 | 
			
		||||
        # Test 13f: Yet another random effect
 | 
			
		||||
        client.light_command(
 | 
			
		||||
            key=rgbcw_light.key, effect="Yet Another Random Effect To Test Memory"
 | 
			
		||||
        )
 | 
			
		||||
        state = await wait_for_state_change(rgbcw_light.key)
 | 
			
		||||
        assert state.effect == "Yet Another Random Effect To Test Memory"
 | 
			
		||||
 | 
			
		||||
        # Test 14: stop effect
 | 
			
		||||
        client.light_command(key=rgbcw_light.key, effect="None")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										102
									
								
								tests/integration/test_scheduler_removed_item_race.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								tests/integration/test_scheduler_removed_item_race.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
"""Test for scheduler race condition where removed items still execute."""
 | 
			
		||||
 | 
			
		||||
import asyncio
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from .types import APIClientConnectedFactory, RunCompiledFunction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_scheduler_removed_item_race(
 | 
			
		||||
    yaml_config: str,
 | 
			
		||||
    run_compiled: RunCompiledFunction,
 | 
			
		||||
    api_client_connected: APIClientConnectedFactory,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that items marked for removal don't execute.
 | 
			
		||||
 | 
			
		||||
    This test verifies the fix for a race condition where:
 | 
			
		||||
    1. cleanup_() only removes items from the front of the heap
 | 
			
		||||
    2. Items in the middle of the heap marked for removal still execute
 | 
			
		||||
    3. This causes cancelled timeouts to run when they shouldn't
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    loop = asyncio.get_running_loop()
 | 
			
		||||
    test_complete_future: asyncio.Future[bool] = loop.create_future()
 | 
			
		||||
 | 
			
		||||
    # Track test results
 | 
			
		||||
    test_passed = False
 | 
			
		||||
    removed_executed = 0
 | 
			
		||||
    normal_executed = 0
 | 
			
		||||
 | 
			
		||||
    # Patterns to match
 | 
			
		||||
    race_pattern = re.compile(r"RACE: .* executed after being cancelled!")
 | 
			
		||||
    passed_pattern = re.compile(r"TEST PASSED")
 | 
			
		||||
    failed_pattern = re.compile(r"TEST FAILED")
 | 
			
		||||
    complete_pattern = re.compile(r"=== Test Complete ===")
 | 
			
		||||
    normal_count_pattern = re.compile(r"Normal items executed: (\d+)")
 | 
			
		||||
    removed_count_pattern = re.compile(r"Removed items executed: (\d+)")
 | 
			
		||||
 | 
			
		||||
    def check_output(line: str) -> None:
 | 
			
		||||
        """Check log output for test results."""
 | 
			
		||||
        nonlocal test_passed, removed_executed, normal_executed
 | 
			
		||||
 | 
			
		||||
        if race_pattern.search(line):
 | 
			
		||||
            # Race condition detected - a cancelled item executed
 | 
			
		||||
            test_passed = False
 | 
			
		||||
 | 
			
		||||
        if passed_pattern.search(line):
 | 
			
		||||
            test_passed = True
 | 
			
		||||
        elif failed_pattern.search(line):
 | 
			
		||||
            test_passed = False
 | 
			
		||||
 | 
			
		||||
        normal_match = normal_count_pattern.search(line)
 | 
			
		||||
        if normal_match:
 | 
			
		||||
            normal_executed = int(normal_match.group(1))
 | 
			
		||||
 | 
			
		||||
        removed_match = removed_count_pattern.search(line)
 | 
			
		||||
        if removed_match:
 | 
			
		||||
            removed_executed = int(removed_match.group(1))
 | 
			
		||||
 | 
			
		||||
        if not test_complete_future.done() and complete_pattern.search(line):
 | 
			
		||||
            test_complete_future.set_result(True)
 | 
			
		||||
 | 
			
		||||
    async with (
 | 
			
		||||
        run_compiled(yaml_config, line_callback=check_output),
 | 
			
		||||
        api_client_connected() as client,
 | 
			
		||||
    ):
 | 
			
		||||
        # Verify we can connect
 | 
			
		||||
        device_info = await client.device_info()
 | 
			
		||||
        assert device_info is not None
 | 
			
		||||
        assert device_info.name == "scheduler-removed-item-race"
 | 
			
		||||
 | 
			
		||||
        # List services
 | 
			
		||||
        _, services = await asyncio.wait_for(
 | 
			
		||||
            client.list_entities_services(), timeout=5.0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Find run_test service
 | 
			
		||||
        run_test_service = next((s for s in services if s.name == "run_test"), None)
 | 
			
		||||
        assert run_test_service is not None, "run_test service not found"
 | 
			
		||||
 | 
			
		||||
        # Execute the test
 | 
			
		||||
        client.execute_service(run_test_service, {})
 | 
			
		||||
 | 
			
		||||
        # Wait for test completion
 | 
			
		||||
        try:
 | 
			
		||||
            await asyncio.wait_for(test_complete_future, timeout=5.0)
 | 
			
		||||
        except TimeoutError:
 | 
			
		||||
            pytest.fail("Test did not complete within timeout")
 | 
			
		||||
 | 
			
		||||
        # Verify results
 | 
			
		||||
        assert test_passed, (
 | 
			
		||||
            f"Test failed! Removed items executed: {removed_executed}, "
 | 
			
		||||
            f"Normal items executed: {normal_executed}"
 | 
			
		||||
        )
 | 
			
		||||
        assert removed_executed == 0, (
 | 
			
		||||
            f"Cancelled items should not execute, but {removed_executed} did"
 | 
			
		||||
        )
 | 
			
		||||
        assert normal_executed == 4, (
 | 
			
		||||
            f"Expected 4 normal items to execute, got {normal_executed}"
 | 
			
		||||
        )
 | 
			
		||||
@@ -705,3 +705,48 @@ def test_empty_or_null_device_id_on_entity() -> None:
 | 
			
		||||
    config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None}
 | 
			
		||||
    validated2 = validator(config2)
 | 
			
		||||
    assert validated2 == config2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_entity_duplicate_validator_non_ascii_names() -> None:
 | 
			
		||||
    """Test that non-ASCII names show helpful error messages."""
 | 
			
		||||
    # Create validator for binary_sensor platform
 | 
			
		||||
    validator = entity_duplicate_validator("binary_sensor")
 | 
			
		||||
 | 
			
		||||
    # First Russian sensor should pass
 | 
			
		||||
    config1 = {CONF_NAME: "Датчик открытия основного крана"}
 | 
			
		||||
    validated1 = validator(config1)
 | 
			
		||||
    assert validated1 == config1
 | 
			
		||||
 | 
			
		||||
    # Second Russian sensor with different text but same ASCII conversion should fail
 | 
			
		||||
    config2 = {CONF_NAME: "Датчик закрытия основного крана"}
 | 
			
		||||
    with pytest.raises(
 | 
			
		||||
        Invalid,
 | 
			
		||||
        match=re.compile(
 | 
			
		||||
            r"Duplicate binary_sensor entity with name 'Датчик закрытия основного крана' found.*"
 | 
			
		||||
            r"Original names: 'Датчик закрытия основного крана' and 'Датчик открытия основного крана'.*"
 | 
			
		||||
            r"Both convert to ASCII ID: '_______________________________'.*"
 | 
			
		||||
            r"To fix: Add unique ASCII characters \(e\.g\., '1', '2', or 'A', 'B'\)",
 | 
			
		||||
            re.DOTALL,
 | 
			
		||||
        ),
 | 
			
		||||
    ):
 | 
			
		||||
        validator(config2)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None:
 | 
			
		||||
    """Test that identical names don't show the enhanced message."""
 | 
			
		||||
    # Create validator for sensor platform
 | 
			
		||||
    validator = entity_duplicate_validator("sensor")
 | 
			
		||||
 | 
			
		||||
    # First entity should pass
 | 
			
		||||
    config1 = {CONF_NAME: "Temperature"}
 | 
			
		||||
    validated1 = validator(config1)
 | 
			
		||||
    assert validated1 == config1
 | 
			
		||||
 | 
			
		||||
    # Second entity with exact same name should fail without enhanced message
 | 
			
		||||
    config2 = {CONF_NAME: "Temperature"}
 | 
			
		||||
    with pytest.raises(
 | 
			
		||||
        Invalid,
 | 
			
		||||
        match=r"Duplicate sensor entity with name 'Temperature' found.*"
 | 
			
		||||
        r"Each entity on a device must have a unique name within its platform\.$",
 | 
			
		||||
    ):
 | 
			
		||||
        validator(config2)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										220
									
								
								tests/unit_tests/test_writer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								tests/unit_tests/test_writer.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,220 @@
 | 
			
		||||
"""Test writer module functionality."""
 | 
			
		||||
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
from unittest.mock import MagicMock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome.storage_json import StorageJSON
 | 
			
		||||
from esphome.writer import storage_should_clean, update_storage_json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def create_storage() -> Callable[..., StorageJSON]:
 | 
			
		||||
    """Factory fixture to create StorageJSON instances."""
 | 
			
		||||
 | 
			
		||||
    def _create(
 | 
			
		||||
        loaded_integrations: list[str] | None = None, **kwargs: Any
 | 
			
		||||
    ) -> StorageJSON:
 | 
			
		||||
        return StorageJSON(
 | 
			
		||||
            storage_version=kwargs.get("storage_version", 1),
 | 
			
		||||
            name=kwargs.get("name", "test"),
 | 
			
		||||
            friendly_name=kwargs.get("friendly_name", "Test Device"),
 | 
			
		||||
            comment=kwargs.get("comment"),
 | 
			
		||||
            esphome_version=kwargs.get("esphome_version", "2025.1.0"),
 | 
			
		||||
            src_version=kwargs.get("src_version", 1),
 | 
			
		||||
            address=kwargs.get("address", "test.local"),
 | 
			
		||||
            web_port=kwargs.get("web_port", 80),
 | 
			
		||||
            target_platform=kwargs.get("target_platform", "ESP32"),
 | 
			
		||||
            build_path=kwargs.get("build_path", "/build"),
 | 
			
		||||
            firmware_bin_path=kwargs.get("firmware_bin_path", "/firmware.bin"),
 | 
			
		||||
            loaded_integrations=set(loaded_integrations or []),
 | 
			
		||||
            loaded_platforms=kwargs.get("loaded_platforms", set()),
 | 
			
		||||
            no_mdns=kwargs.get("no_mdns", False),
 | 
			
		||||
            framework=kwargs.get("framework", "arduino"),
 | 
			
		||||
            core_platform=kwargs.get("core_platform", "esp32"),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return _create
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_clean_when_old_is_none(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is triggered when old storage is None."""
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi"])
 | 
			
		||||
    assert storage_should_clean(None, new) is True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_clean_when_src_version_changes(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is triggered when src_version changes."""
 | 
			
		||||
    old = create_storage(loaded_integrations=["api", "wifi"], src_version=1)
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi"], src_version=2)
 | 
			
		||||
    assert storage_should_clean(old, new) is True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_clean_when_build_path_changes(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is triggered when build_path changes."""
 | 
			
		||||
    old = create_storage(loaded_integrations=["api", "wifi"], build_path="/build1")
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi"], build_path="/build2")
 | 
			
		||||
    assert storage_should_clean(old, new) is True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_clean_when_component_removed(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is triggered when a component is removed."""
 | 
			
		||||
    old = create_storage(
 | 
			
		||||
        loaded_integrations=["api", "wifi", "bluetooth_proxy", "esp32_ble_tracker"]
 | 
			
		||||
    )
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi", "esp32_ble_tracker"])
 | 
			
		||||
    assert storage_should_clean(old, new) is True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_clean_when_multiple_components_removed(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is triggered when multiple components are removed."""
 | 
			
		||||
    old = create_storage(
 | 
			
		||||
        loaded_integrations=["api", "wifi", "ota", "web_server", "logger"]
 | 
			
		||||
    )
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi", "logger"])
 | 
			
		||||
    assert storage_should_clean(old, new) is True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_not_clean_when_nothing_changes(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is not triggered when nothing changes."""
 | 
			
		||||
    old = create_storage(loaded_integrations=["api", "wifi", "logger"])
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi", "logger"])
 | 
			
		||||
    assert storage_should_clean(old, new) is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_not_clean_when_component_added(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is not triggered when a component is only added."""
 | 
			
		||||
    old = create_storage(loaded_integrations=["api", "wifi"])
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi", "ota"])
 | 
			
		||||
    assert storage_should_clean(old, new) is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_should_not_clean_when_other_fields_change(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that clean is not triggered when non-relevant fields change."""
 | 
			
		||||
    old = create_storage(
 | 
			
		||||
        loaded_integrations=["api", "wifi"],
 | 
			
		||||
        friendly_name="Old Name",
 | 
			
		||||
        esphome_version="2024.12.0",
 | 
			
		||||
    )
 | 
			
		||||
    new = create_storage(
 | 
			
		||||
        loaded_integrations=["api", "wifi"],
 | 
			
		||||
        friendly_name="New Name",
 | 
			
		||||
        esphome_version="2025.1.0",
 | 
			
		||||
    )
 | 
			
		||||
    assert storage_should_clean(old, new) is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_edge_case_empty_integrations(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test edge case when old has integrations but new has none."""
 | 
			
		||||
    old = create_storage(loaded_integrations=["api", "wifi"])
 | 
			
		||||
    new = create_storage(loaded_integrations=[])
 | 
			
		||||
    assert storage_should_clean(old, new) is True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_edge_case_from_empty_integrations(
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test edge case when old has no integrations but new has some."""
 | 
			
		||||
    old = create_storage(loaded_integrations=[])
 | 
			
		||||
    new = create_storage(loaded_integrations=["api", "wifi"])
 | 
			
		||||
    assert storage_should_clean(old, new) is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.clean_build")
 | 
			
		||||
@patch("esphome.writer.StorageJSON")
 | 
			
		||||
@patch("esphome.writer.storage_path")
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_update_storage_json_logging_when_old_is_none(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    mock_storage_path: MagicMock,
 | 
			
		||||
    mock_storage_json_class: MagicMock,
 | 
			
		||||
    mock_clean_build: MagicMock,
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that update_storage_json doesn't crash when old storage is None.
 | 
			
		||||
 | 
			
		||||
    This is a regression test for the AttributeError that occurred when
 | 
			
		||||
    old was None and we tried to access old.loaded_integrations.
 | 
			
		||||
    """
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_storage_path.return_value = "/test/path"
 | 
			
		||||
    mock_storage_json_class.load.return_value = None  # Old storage is None
 | 
			
		||||
 | 
			
		||||
    new_storage = create_storage(loaded_integrations=["api", "wifi"])
 | 
			
		||||
    new_storage.save = MagicMock()  # Mock the save method
 | 
			
		||||
    mock_storage_json_class.from_esphome_core.return_value = new_storage
 | 
			
		||||
 | 
			
		||||
    # Call the function - should not raise AttributeError
 | 
			
		||||
    with caplog.at_level("INFO"):
 | 
			
		||||
        update_storage_json()
 | 
			
		||||
 | 
			
		||||
    # Verify clean_build was called
 | 
			
		||||
    mock_clean_build.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    # Verify the correct log message was used (not the component removal message)
 | 
			
		||||
    assert "Core config or version changed, cleaning build files..." in caplog.text
 | 
			
		||||
    assert "Components removed" not in caplog.text
 | 
			
		||||
 | 
			
		||||
    # Verify save was called
 | 
			
		||||
    new_storage.save.assert_called_once_with("/test/path")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.clean_build")
 | 
			
		||||
@patch("esphome.writer.StorageJSON")
 | 
			
		||||
@patch("esphome.writer.storage_path")
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_update_storage_json_logging_components_removed(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    mock_storage_path: MagicMock,
 | 
			
		||||
    mock_storage_json_class: MagicMock,
 | 
			
		||||
    mock_clean_build: MagicMock,
 | 
			
		||||
    create_storage: Callable[..., StorageJSON],
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that update_storage_json logs removed components correctly."""
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_storage_path.return_value = "/test/path"
 | 
			
		||||
 | 
			
		||||
    old_storage = create_storage(loaded_integrations=["api", "wifi", "bluetooth_proxy"])
 | 
			
		||||
    new_storage = create_storage(loaded_integrations=["api", "wifi"])
 | 
			
		||||
    new_storage.save = MagicMock()  # Mock the save method
 | 
			
		||||
 | 
			
		||||
    mock_storage_json_class.load.return_value = old_storage
 | 
			
		||||
    mock_storage_json_class.from_esphome_core.return_value = new_storage
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    with caplog.at_level("INFO"):
 | 
			
		||||
        update_storage_json()
 | 
			
		||||
 | 
			
		||||
    # Verify clean_build was called
 | 
			
		||||
    mock_clean_build.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    # Verify the correct log message was used with component names
 | 
			
		||||
    assert (
 | 
			
		||||
        "Components removed (bluetooth_proxy), cleaning build files..." in caplog.text
 | 
			
		||||
    )
 | 
			
		||||
    assert "Core config or version changed" not in caplog.text
 | 
			
		||||
 | 
			
		||||
    # Verify save was called
 | 
			
		||||
    new_storage.save.assert_called_once_with("/test/path")
 | 
			
		||||
		Reference in New Issue
	
	Block a user