custom-components / ble_monitor

BLE monitor for passive BLE sensors
https://community.home-assistant.io/t/passive-ble-monitor-integration/
MIT License
1.89k stars 243 forks source link

Adding support to the custom Telink firmware for Mi Thermostat (by Atc1441) #117

Closed benkarro closed 3 years ago

benkarro commented 3 years ago

Hi I made this component work with both the original and custom firmware for the LYWSD03MMC sensor. Please take a look at my changes and evaluate if should be included in the component. Changes in sensor.py are marked with arrows.

def parse_raw_message(data, aeskeyslist,  whitelist, report_unknown=False):
    """Parse the raw data."""
    if data is None:
        return None
    # check for Xiaomi service data
    xiaomi_index = data.find(b'\x16\x95\xFE', 15)
    if xiaomi_index == -1:
        return None
    # check for no BR/EDR + LE General discoverable mode flags
> # adv_index = data.find(b"\x02\x01\x06", 14, 17)
>     adv_index1 = data.find(b"\x02\x01\x06", 14, 17)
>     adv_index2 = data.find(b"\x15\x16\x95", 14, 17)
>     
>     if adv_index1 == -1 and adv_index2 == -1:
>         return None
>     elif adv_index1 != -1:
>         adv_index = adv_index1
>     elif adv_index2 != -1:
>         adv_index = adv_index2
# check for BTLE msg size
msg_length = data[2] + 3
if msg_length != len(data):
    return None
# check for MAC presence in message and in service data
xiaomi_mac_reversed = data[xiaomi_index + 8:xiaomi_index + 14]
source_mac_reversed = data[adv_index - 7:adv_index - 1]
if xiaomi_mac_reversed != source_mac_reversed:
    return None
# check for MAC presence in whitelist, if needed
if whitelist:
    if xiaomi_mac_reversed not in whitelist:
        return None
# extract RSSI byte
(rssi,) = struct.unpack("<b", data[msg_length - 1:msg_length])
#strange positive RSSI workaround
if rssi > 0:
    rssi = -rssi
try:
    sensor_type = XIAOMI_TYPE_DICT[
        data[xiaomi_index + 5:xiaomi_index + 7]
    ]
except KeyError:
    if report_unknown:
        _LOGGER.info(
            "BLE ADV from UNKNOWN: RSSI: %s, MAC: %s, ADV: %s",
            rssi,
            ''.join('{:02X}'.format(x) for x in xiaomi_mac_reversed[::-1]),
            data.hex()
        )
    return None
# frame control bits
framectrl, = struct.unpack('>H', data[xiaomi_index + 3:xiaomi_index + 5])
# check data is present
if not (framectrl & 0x4000):
    return None
xdata_length = 0
xdata_point = 0
# check capability byte present
if framectrl & 0x2000:
    xdata_length = -1
    xdata_point = 1
# xiaomi data length = message length
#     -all bytes before XiaomiUUID
#     -3 bytes Xiaomi UUID + ADtype
#     -1 byte rssi
#     -3+1 bytes sensor type
#     -1 byte packet_id
#     -6 bytes MAC
#     - capability byte offset
xdata_length += msg_length - xiaomi_index - 15
if xdata_length < 3:
    return None
xdata_point += xiaomi_index + 14
# check if xiaomi data start and length is valid
if xdata_length != len(data[xdata_point:-1]):
    return None
# check encrypted data flags
if framectrl & 0x0800:
    # try to find encryption key for current device
    try:
        key = aeskeyslist[xiaomi_mac_reversed]
    except KeyError:
        # no encryption key found
        return None
    nonce = b"".join(
        [
            xiaomi_mac_reversed,
            data[xiaomi_index + 5:xiaomi_index + 7],
            data[xiaomi_index + 7:xiaomi_index + 8]
        ]
    )
    decrypted_payload = decrypt_payload(
        data[xdata_point:msg_length-1], key, nonce
    )
    if decrypted_payload is None:
        _LOGGER.error(
            "Decryption failed for %s, decrypted payload is None",
            "".join("{:02X}".format(x) for x in xiaomi_mac_reversed[::-1]),
        )
        return None
    # replace cipher with decrypted data
    msg_length -= len(data[xdata_point:msg_length-1])
    data = b"".join((data[:xdata_point], decrypted_payload, data[-1:]))
    msg_length += len(decrypted_payload)
packet_id = data[xiaomi_index + 7]
result = {
    "rssi": rssi,
    "mac": ''.join('{:02X}'.format(x) for x in xiaomi_mac_reversed[::-1]),
    "type": sensor_type,
    "packet": packet_id,
}
# loop through xiaomi payload
# assume that the data may have several values of different types,
# although I did not notice this behavior with my LYWSDCGQ sensors
while True:
    xvalue_typecode = data[xdata_point:xdata_point + 2]
    try:
        xvalue_length = data[xdata_point + 2]
    except ValueError as error:
        _LOGGER.error("xvalue_length conv. error: %s", error)
        _LOGGER.error("xdata_point: %s", xdata_point)
        _LOGGER.error("data: %s", data.hex())
        result = {}
        break
    except IndexError as error:
        _LOGGER.error("Wrong xdata_point: %s", error)
        _LOGGER.error("xdata_point: %s", xdata_point)
        _LOGGER.error("data: %s", data.hex())
        result = {}
        break
    xnext_point = xdata_point + 3 + xvalue_length
    xvalue = data[xdata_point + 3:xnext_point]
    res = parse_xiaomi_value(xvalue, xvalue_typecode)
    if res:
        result.update(res)
    if xnext_point > msg_length - 3:
        break
    xdata_point = xnext_point
return result
Ernst79 commented 3 years ago

Thanks @benkarro, I've made a pull request, could you check the PR #118 for me by copying the code in your sensor.py and make sure that it works on your LYWSD03MMC, both with the normal firmware as well as the custom firmware. I don't have these, so it is hard to check. And could you give a short explanation what the advantage is of the custom firmware. I will add a note in the readme about the advantage.

If it all works, I will make a new release. Thanks

benkarro commented 3 years ago

Hi

I tested, commented and approved the pull request 118.

About advantages:

With custom firmware its possible to change broadcasting off temperature, humidty and battery to intervals down to 10 sec (from 10 min - 10 sec). Its altso possible to change temp to fahrenheit, display the battery status in the display. Everything can be configured in the website for flashing (https://atc1441.github.io/TelinkFlasher.html).

And there is no use of the encryption key to use in the yaml for the custom component. All in all im pleased to use the custom firmware instead of the original.

Ernst79 commented 3 years ago

Just checking, you don't need a encryption key after flashing with custom firmware?

benkarro commented 3 years ago

Yes! There is no use of the key. I have tested this too. You can see in the picture I have commented away the encryption key after I flashed the sensor and added it to the whitelist without encryption key.

image

ons. 30. sep. 2020 kl. 15:42 skrev Ernst Klamer notifications@github.com:

Just checking, you don't need a encryption key after flashing with custom firmware?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/custom-components/sensor.mitemp_bt/issues/117#issuecomment-701398791, or unsubscribe https://github.com/notifications/unsubscribe-auth/AALUCXNEKKQY35GW7SWMA5TSIMYULANCNFSM4R662WDA .

-- Mvh Amin Ben Karroum

Ernst79 commented 3 years ago

Added in release 0.6.13

nchieffo commented 3 years ago

Do I need to use the firmware option Advertising Type: Mi Like? With Advertising Type: Original I don't get any data in the sensor

benkarro commented 3 years ago

Ye

Do I need to use the firmware option Advertising Type: Mi Like? With Advertising Type: Original I don't get any data in the sensor

Yes. Only tried with Mi Like

nchieffo commented 3 years ago

Ok thanks, and if I understand correctly, I need to reapply this option after changing the battery