From 35c88723c34a2e7fcacbc85baefd5421c5ccd435 Mon Sep 17 00:00:00 2001 From: seviyz Date: Tue, 29 Sep 2026 22:24:10 +0800 Subject: [PATCH] Add support for Xiaomi MJWSD07MMC --- .../ble_monitor/ble_parser/xiaomi.py | 30 ++++++++ custom_components/ble_monitor/const.py | 2 + .../ble_monitor/test/test_xiaomi_parser.py | 70 ++++++++++++++++++- docs/_devices/Xiaomi_MJWSD07MMC.md | 19 +++++ 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 docs/_devices/Xiaomi_MJWSD07MMC.md diff --git a/custom_components/ble_monitor/ble_parser/xiaomi.py b/custom_components/ble_monitor/ble_parser/xiaomi.py index 620eb879..7ec9e354 100644 --- a/custom_components/ble_monitor/ble_parser/xiaomi.py +++ b/custom_components/ble_monitor/ble_parser/xiaomi.py @@ -47,6 +47,7 @@ 0x2832: "MJWSD05MMC", 0x5BEA: "MJWSD06MMC", 0x55B5: "MJWSD06MMC", + 0x9AB9: "MJWSD07MMC", 0x00DB: "MMC-T201-1", 0x0391: "MMC-W505", 0x03DD: "MUE4094RT", @@ -1276,6 +1277,23 @@ def obj5414(xobj): return {"mode": mode} +def obj5c01(xobj): + """Temperature""" + if len(xobj) == 4: + temp = FLOAT_STRUCT.unpack(xobj)[0] + return {"temperature": temp} + else: + return {} + + +def obj5c02(xobj): + """Humidity""" + if len(xobj) == 1: + return {"humidity": xobj[0]} + else: + return {} + + def obj5601(xobj): """Low Battery""" low_batt = xobj[0] @@ -1388,6 +1406,15 @@ def obj5a16(xobj): return {} +def obj601d(xobj): + """CO2""" + if len(xobj) == 2: + (co2,) = H_STRUCT.unpack(xobj) + return {"co2": co2} + else: + return {} + + def obj6012(xobj): """Humidity""" return obj4802(xobj) @@ -1506,11 +1533,14 @@ def obj6e16(xobj): 0x5011: obj5011, 0x5403: obj5403, 0x5414: obj5414, + 0x5c01: obj5c01, + 0x5c02: obj5c02, 0x5601: obj5601, 0x560c: obj560c, 0x560d: obj560d, 0x560e: obj560e, 0x5a16: obj5a16, + 0x601d: obj601d, 0x6012: obj6012, 0x605d: obj605d, 0x6e16: obj6e16 diff --git a/custom_components/ble_monitor/const.py b/custom_components/ble_monitor/const.py index b8c548d8..2b0d158e 100644 --- a/custom_components/ble_monitor/const.py +++ b/custom_components/ble_monitor/const.py @@ -2126,6 +2126,7 @@ class BLEMonitorBinarySensorEntityDescription( 'SV40' : [["battery", "rssi"], [], ["lock", "door", "fingerprint", "antilock", "childlock", "armed away"]], 'MJZNMSQ01YD' : [["battery", "rssi"], [], ["lock", "fingerprint"]], 'MJWSD05MMC' : [["temperature", "humidity", "battery", "rssi"], [], []], + 'MJWSD07MMC' : [["temperature", "humidity", "co2", "battery", "rssi"], [], []], 'XMZNMST02YD' : [["battery", "rssi"], [], ["lock", "door", "fingerprint"]], 'XMZNMS04LM' : [["battery", "rssi"], [], ["lock", "fingerprint"]], 'XMZNMS08LM' : [["battery", "rssi"], [], ["lock", "door"]], @@ -2283,6 +2284,7 @@ class BLEMonitorBinarySensorEntityDescription( 'ZNMS17LM' : 'Xiaomi Aqara', 'MJZNMSQ01YD' : 'Xiaomi', 'MJWSD05MMC' : 'Xiaomi', + 'MJWSD07MMC' : 'Xiaomi', 'MJTZC01YM' : 'Xiaomi', 'XMZNMST02YD' : 'Xiaomi', 'XMZNMS04LM' : 'Xiaomi', diff --git a/custom_components/ble_monitor/test/test_xiaomi_parser.py b/custom_components/ble_monitor/test/test_xiaomi_parser.py index 3645356a..1cef6c3b 100644 --- a/custom_components/ble_monitor/test/test_xiaomi_parser.py +++ b/custom_components/ble_monitor/test/test_xiaomi_parser.py @@ -3,15 +3,16 @@ import os import time +import pytest from ble_monitor.binary_sensor import BaseBinarySensor from ble_monitor.ble_parser import BleParser from ble_monitor.ble_parser.xiaomi import (obj4e0c, obj4e0d, obj4e0e, obj4e16, obj4e17, obj5a16, obj0010, obj560c, obj560d, obj560e, obj1001, obj1017, obj1019, obj3003, obj4810, obj4850, - obj4851, obj4852, obj5010) -from ble_monitor.const import (BINARY_SENSOR_TYPES, MEASUREMENT_DICT, - SENSOR_TYPES) + obj4851, obj4852, obj5003, obj5010) +from ble_monitor.const import (BINARY_SENSOR_TYPES, MANUFACTURER_DICT, + MEASUREMENT_DICT, SENSOR_TYPES) from ble_monitor.sensor import StateChangedSensor from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import UnitOfTime @@ -34,6 +35,12 @@ def test_obj0010_toothbrush_events(self): assert obj0010(b"\x00\x03\xaa") == {"toothbrush": 1, "counter": 3} assert obj0010(b"\x01\x63\xaa") == {"toothbrush": 0, "score": 99} + def test_obj5003_battery(self): + """Test MJWSD07MMC battery parsing.""" + assert obj5003(b"\x64") == {"battery": 100} + assert obj5003(b"\x3d") == {"battery": 61} + assert obj5003(b"\x3d\x00") == {"battery": 61} + def test_obj4e0c_ptx_f1_display_click(self): """obj4e0c PTX-F1-Display: unrecognized click is {}; recognized clicks are unchanged.""" assert obj4e0c(bytes([0]), "PTX-F1-Display") == {} @@ -673,6 +680,63 @@ def test_Xiaomi_MJWSD06MMC(self): assert sensor_msg["humidity"] == 39 assert sensor_msg["rssi"] == -58 + def test_Xiaomi_MJWSD07MMC(self): + """Test Xiaomi parser for MJWSD07MMC with encryption.""" + self.aeskeys = {} + aeskey = "d779248e0d41c08dc2eeed4e22e95881" + p_mac = bytes.fromhex("A4C138094463") + p_key = bytes.fromhex(aeskey) + self.aeskeys[p_mac] = p_key + + test_cases = [ + ( + "043E290201000163440938C1A41D020106191695FE5859B99ACF63440938C1A4" + "96A9C0A8020000ECB05864C6", + 207, + {"humidity": 61}, + ), + ( + "043E260201000163440938C1A41A020106161695FE4859B99ADD9DEBB0E417E2" + "B5020000C2C4B47EC6", + 221, + {"temperature": 26.2}, + ), + ( + "043E2A0201000163440938C1A41E0201061A1695FE5859B99ADC63440938C1A4" + "4F51577A52020000F1FF18DEC6", + 220, + {"co2": 532}, + ), + ( + "043E290201000163440938C1A41D020106191695FE5859B99A0163440938C1A4" + "3A721004040000DAACEDC3C6", + 1, + {"battery": 100}, + ), + ] + + for data_string, packet, expected in test_cases: + data = bytes(bytearray.fromhex(data_string)) + # pylint: disable=unused-variable + ble_parser = BleParser(aeskeys=self.aeskeys) + sensor_msg, tracker_msg = ble_parser.parse_raw_data(data) + + assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)" + assert sensor_msg["type"] == "MJWSD07MMC" + assert sensor_msg["mac"] == "A4C138094463" + assert sensor_msg["packet"] == packet + assert sensor_msg["data"] + assert sensor_msg["rssi"] == -58 + for key, value in expected.items(): + if isinstance(value, float): + assert sensor_msg[key] == pytest.approx(value) + else: + assert sensor_msg[key] == value + assert MANUFACTURER_DICT["MJWSD07MMC"] == "Xiaomi" + assert MEASUREMENT_DICT["MJWSD07MMC"][0] == [ + "temperature", "humidity", "co2", "battery", "rssi" + ] + def test_Xiaomi_MUE4094RT(self): """Test Xiaomi parser for MUE4094RT.""" data_string = "043e1c020102010c39b2e870de100201060c1695fe4030dd032403000101c6" diff --git a/docs/_devices/Xiaomi_MJWSD07MMC.md b/docs/_devices/Xiaomi_MJWSD07MMC.md new file mode 100644 index 00000000..a1af6f02 --- /dev/null +++ b/docs/_devices/Xiaomi_MJWSD07MMC.md @@ -0,0 +1,19 @@ +--- +manufacturer: Xiaomi +name: Mijia Smart Temperature and Humidity Monitor 3 Pro +model: MJWSD07MMC +image: MJWSD06MMC.png +physical_description: Compact temperature, humidity, and CO2 monitor +broadcasted_properties: + - temperature + - humidity + - co2 + - battery + - rssi +broadcasted_property_notes: +broadcast_rate: once per minute +active_scan: +encryption_key: true +custom_firmware: +notes: +---