mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add Xiaomi RTCGQ02LM - Mi Motion Sensor 2 (#3186)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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<uint8_t> &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<XiaomiParseResult> 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<XiaomiParseResult> &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; | ||||
| } | ||||
|   | ||||
| @@ -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<float> temperature; | ||||
| @@ -40,6 +41,7 @@ struct XiaomiParseResult { | ||||
|   optional<bool> is_active; | ||||
|   optional<bool> has_motion; | ||||
|   optional<bool> is_light; | ||||
|   optional<bool> 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<uint8_t> &message, XiaomiParseResult &result); | ||||
| 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); | ||||
|   | ||||
							
								
								
									
										36
									
								
								esphome/components/xiaomi_rtcgq02lm/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/xiaomi_rtcgq02lm/__init__.py
									
									
									
									
									
										Normal 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])) | ||||
							
								
								
									
										64
									
								
								esphome/components/xiaomi_rtcgq02lm/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/xiaomi_rtcgq02lm/binary_sensor.py
									
									
									
									
									
										Normal 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])) | ||||
							
								
								
									
										37
									
								
								esphome/components/xiaomi_rtcgq02lm/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/xiaomi_rtcgq02lm/sensor.py
									
									
									
									
									
										Normal 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)) | ||||
							
								
								
									
										91
									
								
								esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										61
									
								
								esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h
									
									
									
									
									
										Normal 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 | ||||
| @@ -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) | | ||||
|          (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). | ||||
| template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user