diff --git a/CODEOWNERS b/CODEOWNERS index 66ea80f8d6..ebbc8732ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -520,6 +520,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz +esphome/components/xiaomi_xmwsdj04mmc/* @medusalix esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 99e250b6fc..eec6a0e327 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(200.0) async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: - # https://github.com/esphome/AsyncTCP/blob/master/library.json - cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") + # https://github.com/ESP32Async/AsyncTCP + cg.add_library("ESP32Async/AsyncTCP", "3.4.4") elif CORE.is_esp8266: - # https://github.com/esphome/ESPAsyncTCP - cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") + # https://github.com/ESP32Async/ESPAsyncTCP + cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 2c1ce17fb3..51e5cfc8ff 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -75,7 +75,11 @@ void CaptivePortal::start() { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { +#ifndef USE_ESP8266 + auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#else auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#endif response->addHeader("Content-Encoding", "gzip"); req->send(response); return; diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 94db7fef50..c78fff824a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { #endif } - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { if (!this->active_) return false; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index e79fa45f1a..30118d2afd 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -51,6 +51,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring // the data remains valid even after the BLE callback returns. The original // param pointer from ESP-IDF is only valid during the callback. +// +// CRITICAL DESIGN NOTE: +// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety. +// DO NOT attempt to optimize by removing these allocations or storing pointers +// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime +// than our event processing, and accessing it after the callback returns would +// result in use-after-free bugs and crashes. class BLEEvent { public: // NOLINTNEXTLINE(readability-identifier-naming) @@ -67,14 +74,20 @@ class BLEEvent { } // Constructor for GATTC events - uses heap allocation - // Creates a copy of the param struct since the original is only valid during the callback + // 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. 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 - // Creates a copy of the param struct since the original is only valid during the callback + // 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. 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); @@ -222,9 +235,15 @@ class BLEEvent { // 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 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(p->notify.value, p->notify.value + p->notify.value_len); @@ -255,9 +274,15 @@ class BLEEvent { // 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 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(p->write.value, p->write.value + p->write.len); diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index c0eb8db4b3..9e5a2bf45c 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,6 +125,6 @@ async def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.32") + cg.add_library("tonia/HeatpumpIR", "1.0.35") if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index f7ef134803..8797d13e7c 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -116,5 +116,5 @@ async def to_code(config): cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) - cg.add_library("esphome/ESP32-audioI2S", "2.2.0") + cg.add_library("esphome/ESP32-audioI2S", "2.3.0") cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index bdc3d971ce..c4598f44b0 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -40,7 +40,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { */ void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { if (request->method() == HTTP_GET) { if (request->url() == "/metrics") return true; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7ae30522f4..becb5bc2c7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -91,7 +91,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); std::string message = de.message_generator_(web_server_, de.source_); - if (this->try_send(message.c_str(), "state")) { + if (this->send(message.c_str(), "state") != DISCARDED) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); } else { @@ -131,7 +131,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * deq_push_back_with_dedup_(source, message_generator); } else { std::string message = message_generator(web_server_, source); - if (!this->try_send(message.c_str(), "state")) { + if (this->send(message.c_str(), "state") == DISCARDED) { deq_push_back_with_dedup_(source, message_generator); } } @@ -171,8 +171,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); }); - es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { - ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); + es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { + ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); }); es->handleRequest(request); @@ -291,14 +291,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f #ifdef USE_WEBSERVER_LOCAL void WebServer::handle_index_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } #elif USE_WEBSERVER_VERSION >= 2 void WebServer::handle_index_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); +#endif // No gzip header here because the HTML file is so small request->send(response); } @@ -317,8 +326,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::handle_css_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } @@ -326,8 +340,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_JS_INCLUDE void WebServer::handle_js_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } @@ -1837,7 +1856,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c } #endif -bool WebServer::canHandle(AsyncWebServerRequest *request) { +bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (request->url() == "/") return true; @@ -1859,12 +1878,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { -#ifdef USE_ARDUINO - // Header needs to be added to interesting header list for it to not be - // nuked by the time we handle the request later. - // Only required in Arduino framework. - request->addInterestingHeader(HEADER_CORS_REQ_PNA); -#endif return true; } #endif @@ -2145,7 +2158,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif } -bool WebServer::isRequestHandlerTrivial() { return false; } +bool WebServer::isRequestHandlerTrivial() const { return false; } void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { this->sorting_entitys_[entity] = SortingComponents{weight, group}; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index f4d6ad8e86..53ee4d1212 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -99,7 +99,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { protected: // surface a couple methods from the base class using AsyncEventSource::handleRequest; - using AsyncEventSource::try_send; + using AsyncEventSource::send; ListEntitiesIterator entities_iterator_; // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory @@ -468,11 +468,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif /// Override the web handler's canHandle method. - bool canHandle(AsyncWebServerRequest *request) override; + bool canHandle(AsyncWebServerRequest *request) const override; /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) + bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming) void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index f50ee59b9c..c17bab2128 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -36,5 +36,7 @@ async def to_code(config): cg.add_library("WiFi", None) cg.add_library("FS", None) cg.add_library("Update", None) - # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0") + if CORE.is_esp8266: + cg.add_library("ESP8266WiFi", None) + # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index f876d163bc..641006cb99 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -23,7 +23,7 @@ class MiddlewareHandler : public AsyncWebHandler { public: MiddlewareHandler(AsyncWebHandler *next) : next_(next) {} - bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); } + bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); } void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override { @@ -32,7 +32,7 @@ class MiddlewareHandler : public AsyncWebHandler { void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { next_->handleBody(request, data, len, index, total); } - bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); } + bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); } protected: AsyncWebHandler *next_; @@ -131,12 +131,12 @@ class OTARequestHandler : public AsyncWebHandler { void handleRequest(AsyncWebServerRequest *request) override; void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override; - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { return request->url() == "/update" && request->method() == HTTP_POST; } // NOLINTNEXTLINE(readability-identifier-naming) - bool isRequestHandlerTrivial() override { return false; } + bool isRequestHandlerTrivial() const override { return false; } protected: uint32_t last_ota_progress_{0}; diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index d883c0ca9b..8dafdf11ef 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -135,8 +135,8 @@ class AsyncWebServerRequest { return res; } // NOLINTNEXTLINE(readability-identifier-naming) - AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, - const size_t data_size) { + AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data, + const size_t data_size) { auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory) this->init_response_(res, code, content_type); return res; @@ -211,7 +211,7 @@ class AsyncWebHandler { public: virtual ~AsyncWebHandler() {} // NOLINTNEXTLINE(readability-identifier-naming) - virtual bool canHandle(AsyncWebServerRequest *request) { return false; } + virtual bool canHandle(AsyncWebServerRequest *request) const { return false; } // NOLINTNEXTLINE(readability-identifier-naming) virtual void handleRequest(AsyncWebServerRequest *request) {} // NOLINTNEXTLINE(readability-identifier-naming) @@ -220,7 +220,7 @@ class AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} // NOLINTNEXTLINE(readability-identifier-naming) - virtual bool isRequestHandlerTrivial() { return true; } + virtual bool isRequestHandlerTrivial() const { return true; } }; #ifdef USE_WEBSERVER @@ -290,7 +290,7 @@ class AsyncEventSource : public AsyncWebHandler { ~AsyncEventSource() override; // NOLINTNEXTLINE(readability-identifier-naming) - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { return request->method() == HTTP_GET && request->url() == this->url_; } // NOLINTNEXTLINE(readability-identifier-naming) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 04e0724ba7..a80daa0b80 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -91,6 +91,13 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % else if ((value_type == 0x4C02) && (value_length == 1)) { result.humidity = data[0]; + } + // XMWSDJ04MMC humidity, 4 bytes, float, 0.1 °C + else if ((value_type == 0x4C08) && (value_length == 4)) { + const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); + float humidity; + std::memcpy(&humidity, &int_number, sizeof(humidity)); + result.humidity = humidity; } else { return false; } @@ -219,6 +226,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted result.type = XiaomiParseResult::TYPE_LYWSD03MMC; result.name = "LYWSD03MMC"; + } else if (device_uuid == 0x1203) { // small square body, e-ink display, encrypted + result.type = XiaomiParseResult::TYPE_XMWSDJ04MMC; + result.name = "XMWSDJ04MMC"; + if (raw.size() == 19) + result.raw_offset -= 6; } else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight result.type = XiaomiParseResult::TYPE_MJYD02YLA; result.name = "MJYD02YLA"; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 6978be97f4..77fb04fd78 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -20,6 +20,7 @@ struct XiaomiParseResult { TYPE_LYWSD02MMC, TYPE_CGG1, TYPE_LYWSD03MMC, + TYPE_XMWSDJ04MMC, TYPE_CGD1, TYPE_CGDK2, TYPE_JQJCY01YM, diff --git a/esphome/components/xiaomi_xmwsdj04mmc/__init__.py b/esphome/components/xiaomi_xmwsdj04mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_xmwsdj04mmc/sensor.py b/esphome/components/xiaomi_xmwsdj04mmc/sensor.py new file mode 100644 index 0000000000..b41a775f35 --- /dev/null +++ b/esphome/components/xiaomi_xmwsdj04mmc/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BINDKEY, + CONF_HUMIDITY, + CONF_ID, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@medusalix"] +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_xmwsdj04mmc_ns = cg.esphome_ns.namespace("xiaomi_xmwsdj04mmc") +XiaomiXMWSDJ04MMC = xiaomi_xmwsdj04mmc_ns.class_( + "XiaomiXMWSDJ04MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiXMWSDJ04MMC), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + 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)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp new file mode 100644 index 0000000000..f8712e7fd4 --- /dev/null +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp @@ -0,0 +1,77 @@ +#include "xiaomi_xmwsdj04mmc.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_xmwsdj04mmc { + +static const char *const TAG = "xiaomi_xmwsdj04mmc"; + +void XiaomiXMWSDJ04MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (res->humidity.has_value() && this->humidity_ != nullptr) { + // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 + *res->humidity = trunc(*res->humidity); + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + return success; +} + +void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) { + memset(this->bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + this->bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_xmwsdj04mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h new file mode 100644 index 0000000000..9ce02bb64e --- /dev/null +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_xmwsdj04mmc { + +class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_xmwsdj04mmc +} // namespace esphome + +#endif diff --git a/platformio.ini b/platformio.ini index a3c990066a..96926eadd1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -65,14 +65,14 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base mikalhart/TinyGPSPlus@1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.9 ; midea - tonia/HeatpumpIR@1.0.32 ; heatpumpir + tonia/HeatpumpIR@1.0.35 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO @@ -100,7 +100,7 @@ lib_deps = ${common:arduino.lib_deps} ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) - esphome/ESPAsyncTCP-esphome@2.0.0 ; async_tcp + ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) @@ -130,12 +130,12 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@2.1.4 ; async_tcp + ESP32Async/AsyncTCP@3.4.4 ; async_tcp WiFiClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - esphome/ESP32-audioI2S@2.2.0 ; i2s_audio + esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard esphome/esp-audio-libs@1.1.4 ; audio diff --git a/tests/components/xiaomi_xmwsdj04mmc/common.yaml b/tests/components/xiaomi_xmwsdj04mmc/common.yaml new file mode 100644 index 0000000000..fe7a11efc5 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_xmwsdj04mmc + mac_address: 84:B4:DB:5D:A3:8F + bindkey: d8ca2ed09bb5541dc8f045ca360b00ea + temperature: + name: Xiaomi XMWSDJ04MMC Temperature + humidity: + name: Xiaomi XMWSDJ04MMC Humidity + battery_level: + name: Xiaomi XMWSDJ04MMC Battery Level diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml