From da336247eb853c02a5d977bbcfb0914d8f87df68 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 12 Apr 2022 16:19:16 +1200 Subject: [PATCH] Add Xiaomi RTCGQ02LM - Mi Motion Sensor 2 (#3186) --- CODEOWNERS | 1 + esphome/components/xiaomi_ble/xiaomi_ble.cpp | 97 +++++++++++-------- esphome/components/xiaomi_ble/xiaomi_ble.h | 8 +- .../components/xiaomi_rtcgq02lm/__init__.py | 36 +++++++ .../xiaomi_rtcgq02lm/binary_sensor.py | 64 ++++++++++++ esphome/components/xiaomi_rtcgq02lm/sensor.py | 37 +++++++ .../xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp | 91 +++++++++++++++++ .../xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h | 61 ++++++++++++ esphome/core/helpers.h | 4 + tests/test2.yaml | 17 ++++ 10 files changed, 373 insertions(+), 43 deletions(-) create mode 100644 esphome/components/xiaomi_rtcgq02lm/__init__.py create mode 100644 esphome/components/xiaomi_rtcgq02lm/binary_sensor.py create mode 100644 esphome/components/xiaomi_rtcgq02lm/sensor.py create mode 100644 esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp create mode 100644 esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h diff --git a/CODEOWNERS b/CODEOWNERS index 309f6f4d51..5a1220354a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -224,4 +224,5 @@ esphome/components/whirlpool/* @glmnet esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs +esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xpt2046/* @numo68 diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index bdd745b859..95d97defe2 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -1,6 +1,6 @@ #include "xiaomi_ble.h" -#include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #ifdef USE_ESP32 @@ -12,67 +12,74 @@ namespace xiaomi_ble { static const char *const TAG = "xiaomi_ble"; -bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { +bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { + // button pressed, 3 bytes, only byte 3 is used for supported devices so far + if ((value_type == 0x1001) && (value_length == 3)) { + result.button_press = data[2] == 0; + return true; + } // motion detection, 1 byte, 8-bit unsigned integer - if ((value_type == 0x03) && (value_length == 1)) { + else if ((value_type == 0x0003) && (value_length == 1)) { result.has_motion = data[0]; } // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - else if ((value_type == 0x04) && (value_length == 2)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1004) && (value_length == 2)) { + const int16_t temperature = encode_uint16(data[1], data[0]); result.temperature = temperature / 10.0f; } // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - else if ((value_type == 0x06) && (value_length == 2)) { - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1006) && (value_length == 2)) { + const int16_t humidity = encode_uint16(data[1], data[0]); result.humidity = humidity / 10.0f; } // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx - else if (((value_type == 0x07) || (value_type == 0x0F)) && (value_length == 3)) { - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + else if (((value_type == 0x1007) || (value_type == 0x000F)) && (value_length == 3)) { + const uint32_t illuminance = encode_uint24(data[2], data[1], data[0]); result.illuminance = illuminance; - result.is_light = illuminance == 100; + result.is_light = illuminance >= 100; if (value_type == 0x0F) result.has_motion = true; } // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x08) && (value_length == 1)) { + else if ((value_type == 0x1008) && (value_length == 1)) { result.moisture = data[0]; } // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - else if ((value_type == 0x09) && (value_length == 2)) { - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1009) && (value_length == 2)) { + const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x0A) && (value_length == 1)) { + else if ((value_type == 0x100A) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - else if ((value_type == 0x0D) && (value_length == 4)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + else if ((value_type == 0x100D) && (value_length == 4)) { + const int16_t temperature = encode_uint16(data[1], data[0]); + const int16_t humidity = encode_uint16(data[3], data[2]); result.temperature = temperature / 10.0f; result.humidity = humidity / 10.0f; } // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 - else if ((value_type == 0x10) && (value_length == 2)) { - const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1010) && (value_length == 2)) { + const uint16_t formaldehyde = encode_uint16(data[1], data[0]); result.formaldehyde = formaldehyde / 100.0f; } // on/off state, 1 byte, 8-bit unsigned integer - else if ((value_type == 0x12) && (value_length == 1)) { + else if ((value_type == 0x1012) && (value_length == 1)) { result.is_active = data[0]; } // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x13) && (value_length == 1)) { + else if ((value_type == 0x1013) && (value_length == 1)) { result.tablet = data[0]; } // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min - else if ((value_type == 0x17) && (value_length == 4)) { + else if ((value_type == 0x1017) && (value_length == 4)) { const uint32_t idle_time = encode_uint32(data[3], data[2], data[1], data[0]); result.idle_time = idle_time / 60.0f; result.has_motion = !idle_time; + } else if ((value_type == 0x1018) && (value_length == 1)) { + result.is_light = data[0]; } else { return false; } @@ -115,7 +122,7 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult break; } - const uint8_t value_type = payload[payload_offset + 0]; + const uint16_t value_type = encode_uint16(payload[payload_offset + 1], payload[payload_offset + 0]); const uint8_t *data = &payload[payload_offset + 3]; if (parse_xiaomi_value(value_type, data, value_length, result)) @@ -155,60 +162,67 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service result.is_duplicate = false; result.raw_offset = result.has_capability ? 12 : 11; - if ((raw[2] == 0x98) && (raw[3] == 0x00)) { // MiFlora + const uint16_t device_uuid = encode_uint16(raw[3], raw[2]); + + if (device_uuid == 0x0098) { // MiFlora result.type = XiaomiParseResult::TYPE_HHCCJCY01; result.name = "HHCCJCY01"; - } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) { // round body, segment LCD + } else if (device_uuid == 0x01aa) { // round body, segment LCD result.type = XiaomiParseResult::TYPE_LYWSDCGQ; result.name = "LYWSDCGQ"; - } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) { // FlowerPot, RoPot + } else if (device_uuid == 0x015d) { // FlowerPot, RoPot result.type = XiaomiParseResult::TYPE_HHCCPOT002; result.name = "HHCCPOT002"; - } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display + } else if (device_uuid == 0x02df) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display result.type = XiaomiParseResult::TYPE_JQJCY01YM; result.name = "JQJCY01YM"; - } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) { // Philips/Xiaomi BLE nightlight + } else if (device_uuid == 0x03dd) { // Philips/Xiaomi BLE nightlight result.type = XiaomiParseResult::TYPE_MUE4094RT; result.name = "MUE4094RT"; result.raw_offset -= 6; - } else if ((raw[2] == 0x47 && raw[3] == 0x03) || // ClearGrass-branded, round body, e-ink display - (raw[2] == 0x48 && raw[3] == 0x0B)) { // Qingping-branded, round body, e-ink display — with bindkeys + } else if (device_uuid == 0x0347 || // ClearGrass-branded, round body, e-ink display + device_uuid == 0x0B48) { // Qingping-branded, round body, e-ink display — with bindkeys result.type = XiaomiParseResult::TYPE_CGG1; result.name = "CGG1"; - } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden + } else if (device_uuid == 0x03bc) { // VegTrug Grow Care Garden result.type = XiaomiParseResult::TYPE_GCLS002; result.name = "GCLS002"; - } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) { // rectangular body, e-ink display + } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; - } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) { // Mosquito Repellent Smart Version + } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; - } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) { // Cleargrass (Qingping) alarm clock, segment LCD + } else if (device_uuid == 0x0576) { // Cleargrass (Qingping) alarm clock, segment LCD result.type = XiaomiParseResult::TYPE_CGD1; result.name = "CGD1"; - } else if ((raw[2] == 0x6F) && (raw[3] == 0x06)) { // Cleargrass (Qingping) Temp & RH Lite + } else if (device_uuid == 0x066F) { // Cleargrass (Qingping) Temp & RH Lite result.type = XiaomiParseResult::TYPE_CGDK2; result.name = "CGDK2"; - } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) { // small square body, segment LCD, encrypted + } else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted result.type = XiaomiParseResult::TYPE_LYWSD03MMC; result.name = "LYWSD03MMC"; - } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) { // Xiaomi-Yeelight BLE nightlight + } else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight result.type = XiaomiParseResult::TYPE_MJYD02YLA; result.name = "MJYD02YLA"; if (raw.size() == 19) result.raw_offset -= 6; - } else if ((raw[2] == 0xd3) && (raw[3] == 0x06)) { // rectangular body, e-ink display with alarm + } else if (device_uuid == 0x06d3) { // rectangular body, e-ink display with alarm result.type = XiaomiParseResult::TYPE_MHOC303; result.name = "MHOC303"; - } else if ((raw[2] == 0x87) && (raw[3] == 0x03)) { // square body, e-ink display + } else if (device_uuid == 0x0387) { // square body, e-ink display result.type = XiaomiParseResult::TYPE_MHOC401; result.name = "MHOC401"; - } else if ((raw[2] == 0x83) && (raw[3] == 0x0A)) { // Qingping-branded, motion & ambient light sensor + } else if (device_uuid == 0x0A83) { // Qingping-branded, motion & ambient light sensor result.type = XiaomiParseResult::TYPE_CGPR1; result.name = "CGPR1"; if (raw.size() == 19) result.raw_offset -= 6; + } else if (device_uuid == 0x0A8D) { // Xiaomi Mi Motion Sensor 2 + result.type = XiaomiParseResult::TYPE_RTCGQ02LM; + result.name = "RTCGQ02LM"; + if (raw.size() == 19) + result.raw_offset -= 6; } else { ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); return {}; @@ -343,6 +357,9 @@ bool report_xiaomi_results(const optional &result, const std: if (result->is_light.has_value()) { ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); } + if (result->button_press.has_value()) { + ESP_LOGD(TAG, " Button: %s", (*result->button_press) ? "pressed" : ""); + } return true; } diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index ee65d7c82f..399bef83b8 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" #ifdef USE_ESP32 @@ -25,7 +25,8 @@ struct XiaomiParseResult { TYPE_MJYD02YLA, TYPE_MHOC303, TYPE_MHOC401, - TYPE_CGPR1 + TYPE_CGPR1, + TYPE_RTCGQ02LM, } type; std::string name; optional temperature; @@ -40,6 +41,7 @@ struct XiaomiParseResult { optional is_active; optional has_motion; optional is_light; + optional button_press; bool has_data; // 0x40 bool has_capability; // 0x20 bool has_encryption; // 0x08 @@ -61,7 +63,7 @@ struct XiaomiAESVector { size_t ivsize; }; -bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); +bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); diff --git a/esphome/components/xiaomi_rtcgq02lm/__init__.py b/esphome/components/xiaomi_rtcgq02lm/__init__.py new file mode 100644 index 0000000000..0c8331db09 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY + + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32_ble_tracker"] +MULTI_CONF = True + +xiaomi_rtcgq02lm_ns = cg.esphome_ns.namespace("xiaomi_rtcgq02lm") +XiaomiRTCGQ02LM = xiaomi_rtcgq02lm_ns.class_( + "XiaomiRTCGQ02LM", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiRTCGQ02LM), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + } + ) + .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])) diff --git a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py new file mode 100644 index 0000000000..8eee10685e --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_LIGHT, + CONF_MOTION, + CONF_TIMEOUT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOTION, + CONF_ID, +) +from esphome.core import TimePeriod + +from . import XiaomiRTCGQ02LM + +DEPENDENCIES = ["xiaomi_rtcgq02lm"] + +CONF_BUTTON = "button" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(XiaomiRTCGQ02LM), + cv.Optional(CONF_MOTION): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION + ).extend( + { + cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + } + ), + cv.Optional(CONF_LIGHT): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_LIGHT + ), + cv.Optional(CONF_BUTTON): binary_sensor.binary_sensor_schema().extend( + { + cv.Optional(CONF_TIMEOUT, default="200ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + } + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_MOTION in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_MOTION]) + cg.add(parent.set_motion(sens)) + cg.add(parent.set_motion_timeout(config[CONF_MOTION][CONF_TIMEOUT])) + + if CONF_LIGHT in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_LIGHT]) + cg.add(parent.set_light(sens)) + + if CONF_BUTTON in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_BUTTON]) + cg.add(parent.set_button(sens)) + cg.add(parent.set_button_timeout(config[CONF_BUTTON][CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_rtcgq02lm/sensor.py b/esphome/components/xiaomi_rtcgq02lm/sensor.py new file mode 100644 index 0000000000..558e3623e5 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_BATTERY_LEVEL, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + CONF_ID, + DEVICE_CLASS_BATTERY, +) + +from . import XiaomiRTCGQ02LM + +DEPENDENCIES = ["xiaomi_rtcgq02lm"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(XiaomiRTCGQ02LM), + 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, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(parent.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp new file mode 100644 index 0000000000..498e724368 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -0,0 +1,91 @@ +#include "xiaomi_rtcgq02lm.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_rtcgq02lm { + +static const char *const TAG = "xiaomi_rtcgq02lm"; + +void XiaomiRTCGQ02LM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi RTCGQ02LM"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Motion", this->motion_); + LOG_BINARY_SENSOR(" ", "Light", this->light_); + LOG_BINARY_SENSOR(" ", "Button", this->button_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +#endif +} + +bool XiaomiRTCGQ02LM::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 (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } +#ifdef USE_BINARY_SENSOR + if (res->has_motion.has_value() && this->motion_ != nullptr) { + this->motion_->publish_state(*res->has_motion); + this->set_timeout("motion_timeout", this->motion_timeout_, + [this, res]() { this->motion_->publish_state(false); }); + } + if (res->is_light.has_value() && this->light_ != nullptr) + this->light_->publish_state(*res->is_light); + if (res->button_press.has_value() && this->button_ != nullptr) { + this->button_->publish_state(*res->button_press); + this->set_timeout("button_timeout", this->button_timeout_, + [this, res]() { this->button_->publish_state(false); }); + } +#endif +#ifdef USE_SENSOR + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); +#endif + success = true; + } + + return success; +} + +void XiaomiRTCGQ02LM::set_bindkey(const std::string &bindkey) { + memset(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); + bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_rtcgq02lm +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h new file mode 100644 index 0000000000..a16c5209d9 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" +#include "esphome/core/component.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_rtcgq02lm { + +class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { 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; } + +#ifdef USE_BINARY_SENSOR + void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; } + void set_motion_timeout(uint16_t timeout) { this->motion_timeout_ = timeout; } + + void set_light(binary_sensor::BinarySensor *light) { this->light_ = light; } + void set_button(binary_sensor::BinarySensor *button) { this->button_ = button; } + void set_button_timeout(uint16_t timeout) { this->button_timeout_ = timeout; } +#endif + +#ifdef USE_SENSOR + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } +#endif + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + +#ifdef USE_BINARY_SENSOR + uint16_t motion_timeout_; + uint16_t button_timeout_; + + binary_sensor::BinarySensor *motion_{nullptr}; + binary_sensor::BinarySensor *light_{nullptr}; + binary_sensor::BinarySensor *button_{nullptr}; +#endif +#ifdef USE_SENSOR + sensor::Sensor *battery_level_{nullptr}; +#endif +}; + +} // namespace xiaomi_rtcgq02lm +} // namespace esphome + +#endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 074bea6fd1..0972d6ccd6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -173,6 +173,10 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui return (static_cast(byte1) << 24) | (static_cast(byte2) << 16) | (static_cast(byte3) << 8) | (static_cast(byte4)); } +/// Encode a 24-bit value given three bytes in most to least significant byte order. +constexpr uint32_t encode_uint24(uint8_t byte1, uint8_t byte2, uint8_t byte3) { + return ((static_cast(byte1) << 16) | (static_cast(byte2) << 8) | (static_cast(byte3))); +} /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). template::value, int> = 0> diff --git a/tests/test2.yaml b/tests/test2.yaml index ec3ccff70c..a7a9ef9661 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -263,6 +263,10 @@ sensor: name: 'Inkbird IBS-TH1 Humidity' battery_level: name: 'Inkbird IBS-TH1 Battery Level' + - platform: xiaomi_rtcgq02lm + id: motion_rtcgq02lm + battery_level: + name: 'Mi Motion Sensor 2 Battery level' - platform: ltr390 uv: name: "LTR390 UV" @@ -417,6 +421,14 @@ binary_sensor: name: 'CGPR1 Idle Time' illuminance: name: 'CGPR1 Illuminance' + - platform: xiaomi_rtcgq02lm + id: motion_rtcgq02lm + motion: + name: 'Mi Motion Sensor 2' + light: + name: 'Mi Motion Sensor 2 Light' + button: + name: 'Mi Motion Sensor 2 Button' esp32_ble_tracker: on_ble_advertise: @@ -457,6 +469,11 @@ xiaomi_ble: mopeka_ble: +xiaomi_rtcgq02lm: + - id: motion_rtcgq02lm + mac_address: 01:02:03:04:05:06 + bindkey: '48403ebe2d385db8d0c187f81e62cb64' + #esp32_ble_beacon: # type: iBeacon # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98'