Add Xiaomi RTCGQ02LM - Mi Motion Sensor 2 (#3186)

This commit is contained in:
Jesse Hills 2022-04-12 16:19:16 +12:00 committed by GitHub
parent dabd27d4be
commit da336247eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 373 additions and 43 deletions

View file

@ -224,4 +224,5 @@ esphome/components/whirlpool/* @glmnet
esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xpt2046/* @numo68 esphome/components/xpt2046/* @numo68

View file

@ -1,6 +1,6 @@
#include "xiaomi_ble.h" #include "xiaomi_ble.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -12,67 +12,74 @@ namespace xiaomi_ble {
static const char *const TAG = "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 // 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]; result.has_motion = data[0];
} }
// temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C
else if ((value_type == 0x04) && (value_length == 2)) { else if ((value_type == 0x1004) && (value_length == 2)) {
const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); const int16_t temperature = encode_uint16(data[1], data[0]);
result.temperature = temperature / 10.0f; result.temperature = temperature / 10.0f;
} }
// humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 %
else if ((value_type == 0x06) && (value_length == 2)) { else if ((value_type == 0x1006) && (value_length == 2)) {
const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); const int16_t humidity = encode_uint16(data[1], data[0]);
result.humidity = humidity / 10.0f; result.humidity = humidity / 10.0f;
} }
// illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx
else if (((value_type == 0x07) || (value_type == 0x0F)) && (value_length == 3)) { else if (((value_type == 0x1007) || (value_type == 0x000F)) && (value_length == 3)) {
const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); const uint32_t illuminance = encode_uint24(data[2], data[1], data[0]);
result.illuminance = illuminance; result.illuminance = illuminance;
result.is_light = illuminance == 100; result.is_light = illuminance >= 100;
if (value_type == 0x0F) if (value_type == 0x0F)
result.has_motion = true; result.has_motion = true;
} }
// soil moisture, 1 byte, 8-bit unsigned integer, 1 % // 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]; result.moisture = data[0];
} }
// conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm
else if ((value_type == 0x09) && (value_length == 2)) { else if ((value_type == 0x1009) && (value_length == 2)) {
const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); const uint16_t conductivity = encode_uint16(data[1], data[0]);
result.conductivity = conductivity; result.conductivity = conductivity;
} }
// battery, 1 byte, 8-bit unsigned integer, 1 % // 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]; result.battery_level = data[0];
} }
// temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 %
else if ((value_type == 0x0D) && (value_length == 4)) { else if ((value_type == 0x100D) && (value_length == 4)) {
const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); const int16_t temperature = encode_uint16(data[1], data[0]);
const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); const int16_t humidity = encode_uint16(data[3], data[2]);
result.temperature = temperature / 10.0f; result.temperature = temperature / 10.0f;
result.humidity = humidity / 10.0f; result.humidity = humidity / 10.0f;
} }
// formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3
else if ((value_type == 0x10) && (value_length == 2)) { else if ((value_type == 0x1010) && (value_length == 2)) {
const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); const uint16_t formaldehyde = encode_uint16(data[1], data[0]);
result.formaldehyde = formaldehyde / 100.0f; result.formaldehyde = formaldehyde / 100.0f;
} }
// on/off state, 1 byte, 8-bit unsigned integer // 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]; result.is_active = data[0];
} }
// mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % // 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]; result.tablet = data[0];
} }
// idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min // 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]); const uint32_t idle_time = encode_uint32(data[3], data[2], data[1], data[0]);
result.idle_time = idle_time / 60.0f; result.idle_time = idle_time / 60.0f;
result.has_motion = !idle_time; result.has_motion = !idle_time;
} else if ((value_type == 0x1018) && (value_length == 1)) {
result.is_light = data[0];
} else { } else {
return false; return false;
} }
@ -115,7 +122,7 @@ bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult
break; 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]; const uint8_t *data = &payload[payload_offset + 3];
if (parse_xiaomi_value(value_type, data, value_length, result)) if (parse_xiaomi_value(value_type, data, value_length, result))
@ -155,60 +162,67 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
result.is_duplicate = false; result.is_duplicate = false;
result.raw_offset = result.has_capability ? 12 : 11; 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.type = XiaomiParseResult::TYPE_HHCCJCY01;
result.name = "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.type = XiaomiParseResult::TYPE_LYWSDCGQ;
result.name = "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.type = XiaomiParseResult::TYPE_HHCCPOT002;
result.name = "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.type = XiaomiParseResult::TYPE_JQJCY01YM;
result.name = "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.type = XiaomiParseResult::TYPE_MUE4094RT;
result.name = "MUE4094RT"; result.name = "MUE4094RT";
result.raw_offset -= 6; result.raw_offset -= 6;
} else if ((raw[2] == 0x47 && raw[3] == 0x03) || // ClearGrass-branded, round body, e-ink display } else if (device_uuid == 0x0347 || // ClearGrass-branded, round body, e-ink display
(raw[2] == 0x48 && raw[3] == 0x0B)) { // Qingping-branded, round body, e-ink display — with bindkeys device_uuid == 0x0B48) { // Qingping-branded, round body, e-ink display — with bindkeys
result.type = XiaomiParseResult::TYPE_CGG1; result.type = XiaomiParseResult::TYPE_CGG1;
result.name = "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.type = XiaomiParseResult::TYPE_GCLS002;
result.name = "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.type = XiaomiParseResult::TYPE_LYWSD02;
result.name = "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.type = XiaomiParseResult::TYPE_WX08ZM;
result.name = "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.type = XiaomiParseResult::TYPE_CGD1;
result.name = "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.type = XiaomiParseResult::TYPE_CGDK2;
result.name = "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.type = XiaomiParseResult::TYPE_LYWSD03MMC;
result.name = "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.type = XiaomiParseResult::TYPE_MJYD02YLA;
result.name = "MJYD02YLA"; result.name = "MJYD02YLA";
if (raw.size() == 19) if (raw.size() == 19)
result.raw_offset -= 6; 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.type = XiaomiParseResult::TYPE_MHOC303;
result.name = "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.type = XiaomiParseResult::TYPE_MHOC401;
result.name = "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.type = XiaomiParseResult::TYPE_CGPR1;
result.name = "CGPR1"; result.name = "CGPR1";
if (raw.size() == 19) if (raw.size() == 19)
result.raw_offset -= 6; 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 { } else {
ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes.");
return {}; return {};
@ -343,6 +357,9 @@ bool report_xiaomi_results(const optional<XiaomiParseResult> &result, const std:
if (result->is_light.has_value()) { if (result->is_light.has_value()) {
ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); 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; return true;
} }

View file

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -25,7 +25,8 @@ struct XiaomiParseResult {
TYPE_MJYD02YLA, TYPE_MJYD02YLA,
TYPE_MHOC303, TYPE_MHOC303,
TYPE_MHOC401, TYPE_MHOC401,
TYPE_CGPR1 TYPE_CGPR1,
TYPE_RTCGQ02LM,
} type; } type;
std::string name; std::string name;
optional<float> temperature; optional<float> temperature;
@ -40,6 +41,7 @@ struct XiaomiParseResult {
optional<bool> is_active; optional<bool> is_active;
optional<bool> has_motion; optional<bool> has_motion;
optional<bool> is_light; optional<bool> is_light;
optional<bool> button_press;
bool has_data; // 0x40 bool has_data; // 0x40
bool has_capability; // 0x20 bool has_capability; // 0x20
bool has_encryption; // 0x08 bool has_encryption; // 0x08
@ -61,7 +63,7 @@ struct XiaomiAESVector {
size_t ivsize; 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<uint8_t> &message, XiaomiParseResult &result); bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult &result);
optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data);
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address); bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address);

View file

@ -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]))

View file

@ -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]))

View file

@ -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))

View file

@ -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<std::vector<uint8_t> &>(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

View file

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

View file

@ -173,6 +173,10 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui
return (static_cast<uint32_t>(byte1) << 24) | (static_cast<uint32_t>(byte2) << 16) | return (static_cast<uint32_t>(byte1) << 24) | (static_cast<uint32_t>(byte2) << 16) |
(static_cast<uint32_t>(byte3) << 8) | (static_cast<uint32_t>(byte4)); (static_cast<uint32_t>(byte3) << 8) | (static_cast<uint32_t>(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<uint32_t>(byte1) << 16) | (static_cast<uint32_t>(byte2) << 8) | (static_cast<uint32_t>(byte3)));
}
/// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T).
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>

View file

@ -263,6 +263,10 @@ sensor:
name: 'Inkbird IBS-TH1 Humidity' name: 'Inkbird IBS-TH1 Humidity'
battery_level: battery_level:
name: 'Inkbird IBS-TH1 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 - platform: ltr390
uv: uv:
name: "LTR390 UV" name: "LTR390 UV"
@ -417,6 +421,14 @@ binary_sensor:
name: 'CGPR1 Idle Time' name: 'CGPR1 Idle Time'
illuminance: illuminance:
name: 'CGPR1 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: esp32_ble_tracker:
on_ble_advertise: on_ble_advertise:
@ -457,6 +469,11 @@ xiaomi_ble:
mopeka_ble: mopeka_ble:
xiaomi_rtcgq02lm:
- id: motion_rtcgq02lm
mac_address: 01:02:03:04:05:06
bindkey: '48403ebe2d385db8d0c187f81e62cb64'
#esp32_ble_beacon: #esp32_ble_beacon:
# type: iBeacon # type: iBeacon
# uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98'