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/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/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