diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py index 0e84278afa..8ce216da22 100644 --- a/esphome/components/bthome_mithermometer/__init__.py +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg from esphome.components import esp32_ble_tracker import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MAC_ADDRESS +from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS +from esphome.core import HexInt CODEOWNERS = ["@nagyrobi"] DEPENDENCIES = ["esp32_ble_tracker"] @@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None): { cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_BINDKEY): cv.bind_key, } ) .extend(BLE_DEVICE_SCHEMA) @@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if bindkey := config.get(CONF_BINDKEY): + bindkey_bytes = [ + HexInt(int(bindkey[index : index + 2], 16)) + for index in range(0, len(bindkey), 2) + ] + cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes))) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp index d1c5165896..2b73d8735c 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.cpp +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -3,15 +3,23 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include +#include #include #ifdef USE_ESP32 +#include "mbedtls/ccm.h" + namespace esphome { namespace bthome_mithermometer { static const char *const TAG = "bthome_mithermometer"; +static constexpr size_t BTHOME_BINDKEY_SIZE = 16; +static constexpr size_t BTHOME_NONCE_SIZE = 13; +static constexpr size_t BTHOME_MIC_SIZE = 4; +static constexpr size_t BTHOME_COUNTER_SIZE = 4; static const char *format_mac_address(std::span buffer, uint64_t address) { std::array mac{}; @@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() { char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_)); + if (this->has_bindkey_) { + char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)]; + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.')); + } LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev return matched; } +void BTHomeMiThermometer::set_bindkey(std::initializer_list bindkey) { + if (bindkey.size() != sizeof(this->bindkey_)) { + ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size()); + return; + } + std::copy(bindkey.begin(), bindkey.end(), this->bindkey_); + this->has_bindkey_ = true; +} + +bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector &data, uint64_t source_address, + std::vector &payload) const { + if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) { + ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size()); + return false; + } + + const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE; + payload.resize(ciphertext_size); + + std::array mac{}; + for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) { + mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF; + } + + std::array nonce{}; + memcpy(nonce.data(), mac.data(), mac.size()); + nonce[6] = 0xD2; + nonce[7] = 0xFC; + nonce[8] = data[0]; + memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE); + + const uint8_t *ciphertext = data.data() + 1; + const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE; + + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + + int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8); + if (ret) { + ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed."); + mbedtls_ccm_free(&ctx); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext, + payload.data(), mic, BTHOME_MIC_SIZE); + mbedtls_ccm_free(&ctx); + if (ret) { + ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret); + return false; + } + return true; +} + bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, const esp32_ble_tracker::ESPBTDevice &device) { if (!service_data.uuid.contains(0xD2, 0xFC)) { @@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD return false; } - char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - if (is_encrypted) { - ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf)); + uint64_t source_address = device.address_uint64(); + bool address_matches = source_address == this->address_; + if (!is_encrypted && mac_included && data.size() >= 7) { + uint64_t advertised_address = 0; + for (int i = 5; i >= 0; i--) { + advertised_address = (advertised_address << 8) | data[1 + i]; + } + address_matches = address_matches || advertised_address == this->address_; + } + + if (is_encrypted && !this->has_bindkey_) { + if (address_matches) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s", + device.address_str_to(addr_buf)); + } return false; } - size_t payload_index = 1; - uint64_t source_address = device.address_uint64(); + if (!is_encrypted && this->has_bindkey_) { + if (address_matches) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s", + device.address_str_to(addr_buf)); + } + return false; + } + std::vector decrypted_payload; + const uint8_t *payload = nullptr; + size_t payload_size = 0; + + if (is_encrypted) { + if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf)); + return false; + } + payload = decrypted_payload.data(); + payload_size = decrypted_payload.size(); + } else { + payload = data.data() + 1; + payload_size = data.size() - 1; + } if (mac_included) { - if (data.size() < 7) { + if (payload_size < 6) { ESP_LOGVV(TAG, "BTHome payload missing MAC address"); return false; } source_address = 0; for (int i = 5; i >= 0; i--) { - source_address = (source_address << 8) | data[1 + i]; + source_address = (source_address << 8) | payload[i]; } - payload_index = 7; + payload += 6; + payload_size -= 6; } + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; if (source_address != this->address_) { ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address)); return false; } - if (payload_index >= data.size()) { + if (payload_size == 0) { ESP_LOGVV(TAG, "BTHome payload empty after header"); return false; } bool reported = false; - size_t offset = payload_index; + size_t offset = 0; uint8_t last_type = 0; - while (offset < data.size()) { - const uint8_t obj_type = data[offset++]; + while (offset < payload_size) { + const uint8_t obj_type = payload[offset++]; size_t value_length = 0; bool has_length_byte = obj_type == 0x53; // text objects include explicit length if (has_length_byte) { - if (offset >= data.size()) { + if (offset >= payload_size) { break; } - value_length = data[offset++]; + value_length = payload[offset++]; } else { if (!get_bthome_value_length(obj_type, value_length)) { ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type); @@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD break; } - if (offset + value_length > data.size()) { + if (offset + value_length > payload_size) { ESP_LOGVV(TAG, "BTHome object length exceeds payload"); break; } - const uint8_t *value = &data[offset]; + const uint8_t *value = &payload[offset]; offset += value_length; if (obj_type < last_type) { diff --git a/esphome/components/bthome_mithermometer/bthome_ble.h b/esphome/components/bthome_mithermometer/bthome_ble.h index 3d2380b48d..ef3038ec93 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.h +++ b/esphome/components/bthome_mithermometer/bthome_ble.h @@ -5,6 +5,8 @@ #include "esphome/core/component.h" #include +#include +#include #ifdef USE_ESP32 @@ -14,6 +16,7 @@ namespace bthome_mithermometer { class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(std::initializer_list bindkey); void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } @@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi protected: bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, const esp32_ble_tracker::ESPBTDevice &device); + bool decrypt_bthome_payload_(const std::vector &data, uint64_t source_address, + std::vector &payload) const; uint64_t address_{0}; optional last_packet_id_{}; + bool has_bindkey_{false}; + uint8_t bindkey_[16]; sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 6cb204c8de..276ea24717 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r delayMicroseconds(500); } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { delayMicroseconds(2000); - } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { - delayMicroseconds(1000); } else { - delayMicroseconds(800); + delayMicroseconds(1000); } #ifdef USE_ESP32 diff --git a/esphome/components/mipi_dsi/models/m5stack.py b/esphome/components/mipi_dsi/models/m5stack.py index 6055c77f8f..2298f76cd4 100644 --- a/esphome/components/mipi_dsi/models/m5stack.py +++ b/esphome/components/mipi_dsi/models/m5stack.py @@ -55,3 +55,44 @@ DriverChip( (0x35,), (0xFE,), ], ) + +DriverChip( + "M5STACK-TAB5-V2", + height=1280, + width=720, + hsync_back_porch=40, + hsync_pulse_width=2, + hsync_front_porch=40, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=220, + pclk_frequency="80MHz", + lane_bit_rate="960Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0x60, 0x71, 0x23, 0xa2), + (0x60, 0x71, 0x23, 0xa3), + (0x60, 0x71, 0x23, 0xa4), + (0xA4, 0x31), + (0xD7, 0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80), + (0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09), + (0xA3, 0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF), + (0xA6, 0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00), + (0xA7, 0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44), + (0xAC, 0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01), + (0xAD, 0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF), + (0xAE, 0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00), + (0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, 0xC0, 0x00), + (0xE8, 0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E), + (0x75, 0x03, 0x04), + (0xE7, 0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77), + (0xEA, 0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C), + (0xB0, 0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43), + (0xb7, 0x00, 0x00, 0x73, 0x73), + (0xBF, 0xA6, 0xAA), + (0xA9, 0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03), + (0xC8, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF), + (0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF), + ], +) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 72dab987f3..17a800a035 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -638,6 +638,11 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); + // Clear error flag here because restart_adapter() enters COOLDOWN state, + // and check_connecting_finished() is called after cooldown without going + // through start_connecting() first. Without this clear, stale errors would + // trigger spurious "failed (callback)" logs. The canonical clear location + // is in start_connecting(); this is the only exception to that pattern. this->error_from_callback_ = false; } @@ -691,8 +696,6 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; - // Clear error flag before reconnecting so first attempt is not seen as immediate failure - this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -1040,6 +1043,12 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); #endif + // Clear any stale error from previous connection attempt. + // This is the canonical location for clearing the flag since all connection + // attempts go through start_connecting(). The only other clear is in + // restart_adapter() which enters COOLDOWN without calling start_connecting(). + this->error_from_callback_ = false; + if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); // Enter cooldown to allow WiFi hardware to stabilize @@ -1145,7 +1154,6 @@ void WiFiComponent::enable() { return; ESP_LOGD(TAG, "Enabling"); - this->error_from_callback_ = false; this->state_ = WIFI_COMPONENT_STATE_OFF; this->start(); } @@ -1387,11 +1395,6 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; this->num_retried_ = 0; - // Ensure next connection attempt does not inherit error state - // so when WiFi disconnects later we start fresh and don't see - // the first connection as a failure. - this->error_from_callback_ = false; - if (this->has_ap()) { #ifdef USE_CAPTIVE_PORTAL if (this->is_captive_portal_active_()) { @@ -1915,8 +1918,6 @@ void WiFiComponent::retry_connect() { this->advance_to_next_target_or_increment_retry_(); } - this->error_from_callback_ = false; - yield(); // Check if we have a valid target before building params // After exhausting all networks in a phase, selected_sta_index_ may be -1 @@ -2242,7 +2243,6 @@ void WiFiComponent::process_roaming_scan_() { this->roaming_state_ = RoamingState::CONNECTING; // Connect directly - wifi_sta_connect_ handles disconnect internally - this->error_from_callback_ = false; this->start_connecting(roam_params); } diff --git a/tests/components/bthome_mithermometer/common.yaml b/tests/components/bthome_mithermometer/common.yaml index ba94e46878..7a68fae966 100644 --- a/tests/components/bthome_mithermometer/common.yaml +++ b/tests/components/bthome_mithermometer/common.yaml @@ -3,6 +3,7 @@ esp32_ble_tracker: sensor: - platform: bthome_mithermometer mac_address: A4:C1:38:4E:16:78 + bindkey: eef418daf699a0c188f3bfd17e4565d9 temperature: name: "BTHome Temperature" humidity: