Как я данные с BLE-градусника от Xiaomi забирал

    Предыстория: в качестве одного из хобби у меня случился «Умный дом». Хочется красивых устройств, но при этом ещё хочется свободы и приватности. Поэтому занимаюсь скрещиванием ужика Xiaomi с ёжиком Home Assistant.

    Для поддержания комфортных условий нам нужно знать, а что вообще у нас дома происходит. Короче говоря, нужны сенсоры. Их у Xiaomi есть много разных, но больше всего мне понравился квадратный градусник на электронных чернилах. Вот только он совсем не умный, в том смысле, что не предоставляет вообще никаких интерфейсов, кроме графического – ни тебе WiFi, ни BLE, ни ZigBee. Зато батарейки CR2032 хватает на несколько лет. Есть ещё версия с блютусом, но она чуть менее изящная – эдакий толстый блинчик.

    И вот в начале весны был анонсирован новый датчик температуры/влажности, на электронных чернилах, с BLE, да ещё и с часами. Часы мне не особенно-то и нужны, а вот всё остальное немедленно подавило все рациональные доводы и градусник был заказан на одном из популярных интернет-магазинов, по предзаказу. Ехало оно ехало, и наконец приехало.



    В приложение MiHome датчик добавился без проблем (у меня англоязычный интерфейс везде, с русской версией MiHome, говорят, были трудности перевода). Показывает текущие значения и историю изменения показаний.

    А вот с интеграцией в Home Assistant приключились сложности. Имеющийся компонент для датчика температуры ни в какую не хотел забирать данные с устройства и жаловался на неверный формат данных. Ну, делать нечего, достаём лопату и начинаем копать.

    Первой мыслью было ознакомиться с устройством протокола BLE, но оценив размер документации, было принято решение переходить к методу народного тыка.

    Первый подход к снаряду


    Для начала, открываем терминал на убунте и запускаем bluetoothctl. Видим следующее:

    [NEW] Controller 00:1A:7D:DA:71:13 fett [default]
    [NEW] Device 3F:59:C8:80:70:BE LYWSD02
    [NEW] Device 4C:65:A8:DC:0D:AF MJ_HT_V1

    MJ_HT_V1 – это старый датчик температуры, LYWSD02 – новый. Разница в формате именования модели несколько настораживает.

    Дальше надо как-то почитать, а какие данные вообще у нас могут быть получены. Открыл исходники библиотеки mitemp, которая используется в Home Assistant для получения данных со старого датчика. Там нашёл, что используется библиотека blewrap, которая, в свою очередь, является обёрткой над двумя питоновскими библиотеками для работы с BLE. Столько много слоёв мне ни к чему, будем использовать bluepy. Документация есть, её не много и не мало, читаем и пишем скрипт, который проходит по всем полям данных, которые есть на устройстве.

    from bluepy import btle
    
    mac = '3F:59:C8:80:70:BE'
    p = btle.Peripheral(mac)
    
    for s in p.getServices():
        print('Service:', s.uuid)
        for c in s.getCharacteristics():
            print('\tCharacteristic:', c.uuid)
            print('\t\t', c.propertiesToString())
            if c.supportsRead():
                print('\t\t', c.read())

    В целом, всё просто – BLE-устройство предоставляет набор сервисов, каждый из которых состоит из набора характеристик. Каждая характеристика может быть одного из 8 типов, для одной характеристики можно указать несколько типов одновременно. Сервисы и характеристики идентифицируются двумя способами – адресом в виде HEX-значения и UUID. С UUID мне работать как-то привычнее.

    Итак я считал все характеристики для обоих датчиков, посмотрел на них и понял, что снова под маркой Xiaomi продаются устройства от совершенно разных производителей. Среди значений старого датчика было найдено «Cleargrass Inc», а в новом – «miaomiaoce.com». Структура сервисов и характеристик у этих двух датчиков также абсолютно разные, да и список характеристик у нового датчика длиннее в два раза. Тут стало понятно, что нужно писать свою собственную библиотеку для интеграции с датчиком (нет, я конечно сначала погуглил, может есть чего полезного по запросу LYWSD02, но ничего толкового гугл не выдал).

    Как же всё-таки получить данные?


    Среди имеющихся типов характеристик, кроме READ, есть ещё WRITE и NOTIFY. WRITE – для отправки данных на устройство, а NOTIFY – для получения данных. Есть ещё одновременно WRITE NOTIFY – устройство будет отправлять данные только после того, как на них подпишутся, отправив нужный байт командой WRITE.

    Попытки сделать что-то руками не принесли никакого результата, был достигнут первый рубеж отчаяния, но в этот момент я обчитался статей про поделки на базе чипов от Nordic Semiconductors и поставил на смартфон программу nRF Connect. С её помощью я смог подписаться на все сервисы, которые предоставляло устройство, сохранил логи ответов и стал пытаться понять, что в них лежит.



    Вот эти тройные стрелки активируют подписку.

    Особенностью старого датчика было то, что данные о температуре и влажности приходили в виде UTF-строки, новый же всё отдавал в бинарном виде.

    Подписка на уведомления


    Чтобы получать данные с датчика, нужно отправить запрос на подписку. В библиотеке mitemp для этого отправлялись два байта на характеристику, но непонятно, откуда брать её Тут я посмотрел на то, как выглядит структура данных для старого датчика в nRF Connect и заметил, что нужный адрес указан для характеристики с данными, как некий дескриптор. Тогда я снова стал читать документацию к bluepy и понял, что адрес дескриптора легко можно получить из объекта характеристики. Осталось только написать класс с методом-колбеком, в который будут поступать данные из уведомления.

    class MyDelegate(btle.DefaultDelegate):
        def handleNotification(self, cHandle, data):
            print(data)
    
    mac_addr = '3F:59:C8:80:70:BE'
    p = btle.Peripheral(mac_addr)
    p.setDelegate(MyDelegate())
    uuid = 'EBE0CCC1-7A0A-4B0C-8A1A-6FF2997DA3A6'
    
    # Метод всегда возвращает список, потому что может работать с диапазоном адресов
    ch = p.getCharacteristics(uuid=uuid)[0]
    # Получаем дескрипторы для характеристики
    desc = ch.getDescriptors(forUUID=0x2902)[0]
    
    # Значение байта, который нужно отправить был найден методом научного тыка
    desc.write(0x01.to_bytes(2, byteorder="little"), withResponse=True)
    
    while True:
            p.waitForNotifications(5.0)

    Отделяем зёрна от плевел


    К счастью, только три характеристики были помечены как WRITE NOTIFY, при этом данные приходили с различной частотой и, эм..., визуальными особенностями.

    Первый запрос присылал сразу большую портянку данных, а потом залипал. При этом первый байт представлял собой монотонно возрастающее число. Похоже, что это накопленная история средних значений.

    Второй и третий присылались периодически, но приглядевшись, я увидел, что один из них не меняется, а в данных второго меняется только один байт. Ну, значит, это текущее время (напомню, в этом градуснике есть часы. В любом уважающем себя устройстве для умного дома должны быть часы).

    Предположим, то третья характеристика – это полезные данные о температуре и влажности. Для подтверждения гипотезы был проведён физический эксперимент – подошёл к датчику и грубо надышал на него. На дисплее резко увеличились значения данных, а в терминале – поменялись байтики. Ура, данные где-то рядом.

    Разбор данных


    Я обычно работаю с текстовыми данными (получи данные по HTTP в виде JSON/xml, положи их в файл или в базу), поэтому не очень понимал, как подступиться к задаче. Поэтому начал пытаться трансформировать данные разными способами, которые можно сделать из питона. Написал вот такую функцию-конвертилку и стал смотреть, как это соотносится с данными на экране датчика.

    def parse(v):
        print([x for x in v])
        print('{0:#x}'.format(int.from_bytes(data, byteorder='big')))
        print('{0:#x}'.format(int.from_bytes(data, byteorder='little')))

    В консоль посыпались строчки разной степени непонятности, однако третий байт всегда был числом, и это число совпадало со значением влажности. Для верности я ещё раз подышал на датчик – и значения влажности на экране и в третьем байте изменились одинаково!

    Тут я предположил, что в первых двух байтах хранится температура. Для того, чтобы данные менялись – перенёс датчик на полотенцесушитель в ванной. Но, сколько я ни пытался трансформировать результаты – нужных чисел не получалось.

    На пути к успеху


    В этот момент я ещё раз посмотрел на описание датчика и увидел, что внутри стоит датчик от Swiss Sensirion. Наверно, стоило с этого начинать, но это же не наш метод. На сайте Swiss Sensirion была найдена пачка датчиков, и даташиты к ним. В даташите, кроме всего прочего была найдена формула для конвертации байт, передаваемых по шине I2C в число.

    $T[°C] = -45 + 175\cdot\frac{S_T}{2^16-1}$



    Но… Получались очень странные значения. Что-то типа -34.66, а у меня было явно теплее. От грусти и печали я даже вскрыл датчик и проверил, а правда ли там сенсор от Swiss Sensirion. Выяснилось, что правда, но с индексом SHTC3, и для него нужна чуть-чуть другая формула.

    $T[°C] = -45 + 175\cdot\frac{S_T}{2^16}$



    Однако, всё равно данные после преобразования даже близко не были похожи на реальные. Тут я ещё больше загрустил, открыл исходники библиотеки для SHTC3 от Adafruit и стал пытаться адаптировать код трансформации из C++ в питон. Вывел всё в табличку – сырые данные, преобразованную сишную структуру и результат.

    def handleNotification(self, cHandle, data):
        temp = data[:2]
        humid = data[2]
        unpacked = struct.unpack('H', temp)[0]
        print(data, unpacked, -45 + 175 * unpacked / 2 ** 19, sep='\t')

    Получил что-то такое:

    b',\n2'	2604	  -44.130821228027344
    b'-\n2'	2605	  -44.1304874420166
    b'+\n2'	2603	  -44.131155014038086
    b',\n2'	2604	  -44.130821228027344

    Да… как-то холодно… Но, стоп, подождите, а что это за 2604? Это же оно, 26.0 градусов на экране! Для подтверждения гипотезы снова унёс датчик на батарею, проверил – значения совпадают.

    В итоге, получаем такой код преобразования данных:

    def handleNotification(self, cHandle, data):
        humid_bytes = data[2]
        temp_bytes = data[:2]
        humidity = humid_bytes
        temperature = struct.unpack('H', temp_bytes)[0] / 100
        
        print(temperature, humidity)
    

    Эпилог


    На операции по подключению к датчику и поиски правильного алгоритма трансформации ушла пара вечеров. Несколько раз хотелось всё бросить, но одновременно с этим приходили новые идеи и я продолжал попытки.

    Сейчас данные передаются в Home Assistant, дальше нужно допилить код интеграции и, возможно, переписать её с bluepy на bleak, поскольку bleak использует async/await и лучше подойдёт для Home Assistant, написанного aiohttp.



    Ссылки:


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 41

      0
      У данного девайса на сколько хватает батарейки? И второй вопрос, можно ли «сбоку» подцепится к экрану и что-то выводить, или через тот-же ble?
        0
        На сколько батарейки хватит – пока не знаю, девайс всего полторы недели у меня в руках. Питается от двух CR2032 и ещё одна маленькая батарейка для RTC глубоко внутри.

        Выводить что-то своё скорее всего проблематично. Если только перепрошивать микроконтроллер.
          0
          Я у его младшего собрата батарейку уже второй раз меняю за год.
            0
            Тот, который квадратный или тот, который круглый? У этого и у круглого можно получать информацию о разряде батареи и подготовиться заранее, так сказать.
              0
              Квадратный. Но возможно комплектная батарейка была подсевшая, и первый раз я сменил её на батарейку из своих старых запасов — фиг знает сколько ей лет было. И вот сейчас третья стоит свеже купленная — посмотрим.
                0
                У меня квадратный датчик (приобрел его в декабре) отключился оставив на экране значок перечеркнутой батареи. Я вытащил батарейку (что шла в комплекте), вставил её обратно — проработал датчик еще два месяца. Сегодня снова её перевоткнул — пока работает.
                ¯\_(ツ)_/¯
                  0
                  Я пробовал, у меня не получилось, через 5 минут отключился опять.
          0
          практически уверен что экран там сегментный а не пиксельный (в терминологии могу ошибиться)
          0
          Вот если б к ардуино делали красивые корпуса, то можно было б собрать красивый датчик самому, и не надо возиться со странным протоколом ;-)

          А на какое расстояние «добивает» сигнал?
            0
            Метров пять, не больше. И это если нет препятствий по дороге (закрытая дверь, например).
              0
              Очень плохо. Возможно, Вы что-то не так сделали. Круглый LCD BLE у меня пробивает две не толстых стены и это примерно 7 метров, а mi flora сейчас вообще стоят на балконе и пробивают две стенки плюс третью несущую. В качестве шлюза малина со своим встроенным блютуз. Кстати, те же mi flora на прямой видимости пробивают ~30 метров, в деревне относил их в теплицу, из дома возле окна данные приходили, мне кажется и 40-50 метров на прямую смогут.
                0
                Я, честно говоря, не замерял RSSI, плюс ещё всё очень сильно всё зависит от приёмника, и от того, насколько сильно загрязнён эфир на 2.4ГГц. Во всяком случае, во время опытов в ванной при закрытой двери нормально получалось подключаться где-то один раз из пяти. С отрытой дверь всё ок.
            0
            Есть ли возможность сделать фото платы? Или выложить уже имеющиеся?
            0
            А почему не использовать отдельно красивый Е-инк термометр и BLE термометр-таблетку без экранчика которая? Я сделал именно так.
              0

              Ссылки? Цены?

                0
                  0
                  E-Ink у меня есть, это вот как раз тот, который туповат – умеет только показывать данные, без каких либо беспроводных интерфейсов.

                  Таблетка тоже есть, но в другой комнате. И она с ZigBee, так что кроме неё нужно ещё и гейт ставить.
                    0
                    Я думал раз у вас умный дом с сяоми, то гейт-то уж точно есть…
                      0
                      Гейт есть. Под него даже специально китайская розетка была куплена, чтобы подключаться в сеть без переходника. Но решения без гейта мне нравятся значительно больше.
                        0
                        вместо гейта может быть сс2531
                    0
                    Вот эта таблетка (квадратная), которая ZigBee, очень тупая вещь для автоматизации. Как она данные шлёт я так и не понял. Может за 10 минут три раза прислать показания различающиеся всего на десятые доли, потом может 40(!) минут молчать и плюнуть показания на десяток больше\меньше. Я его на кухне ставил, пытался по влажности определять закипание воды, сейчас в период отключения горячей актуально. На NodeRED сделал алгоритм, который высчитывает разницу между показаниями и решает дать оповещение или рано. Так при начальной влажности 40% он мне прислал в течении 13 минут показания 40, 41, 41, потом молчал 27 минут и выплюнул 56%. У меня два таких датчика, работают одинаково. Никому не рекомендую их.
                    То ли дело BLE с LCD, любое изменение — отослал на сервер, а измеряет он каждую секунду. Вот с ним алгоритм заработал.
                      0
                      А таблетка разве не ZigBee?
                        0
                        ZigBee. Ну я выразился неверно. Не BLE а беспроводной в смысле.
                    0
                    Ну а зачем два устройства, если можно обойтись одним. Да и самому интересно было раскопать, как данные передаются.
                      0
                      Ну по мне так квадратный выглядит гораздо приятнее часов. Так как последний это по-сути часы с функцией термометра/гигрометра и показания там мелкие. А часы у меня хорошие есть тоже Сяоми, отдельно.
                        +1
                        Кстати сейчас искал на али и наткнулся на совсем новый – тут и BLE и часов нет и дизайн приятный.
                          0
                          И рожицы нет!
                            0
                            Вот мы его искали в своё время всем офисом, но посчитали, что его только анонсировали, но не выпустили.
                              0
                              Да, он видимо совсем свежий.
                        +1
                        А вообще есть weather station не BLE, но к которым можно достучаться по http, например, для снятия показаний сенсоров? Wi-Fi или кабель не так важно.
                          0
                          Наверняка есть, но WiFi ест много энергии, а BLE/ZibBee работают от батарейки месяцами.
                          0
                          Не совсем понял какой конкретно датчик имеется в виду под старым. Маленький и круглый? Если да, то можно ли его напрямую подключить к home assistent?
                          0
                          читал, читал, но не понял — можно ли логировать данные, которые поступают с датчика, в самой программе nrf connect?
                          Я сейчам смотрю на немного другой датчик. У него есть сервис Indicate. Не знаете что это? (есть толь read; write; notify и notify, write).
                            0
                            nRF connect пишет логи, как их забирать – см. в документации к nRF Connect, там всё зависит от операционки смартфона. На айфоне надо «смахнуть» экран вправо, находясь на странице устройства или его характеристик.

                            image
                            0
                            А как время на часах через BLE выставить не исследовали?
                              +1
                              Там нужно послать WRITE-команду на одну из характеристик, детально не разбирался, жду, когда второй датчик приедет, тогда домучаю библиотеку, чтобы можно было и время менять, и историю разобрать. И цельсий на фаренгейты менять ;)
                                0
                                Да, понятно, что характеристику изменить надо. Интересно, что там за кодирование.
                                Забавная игрушка, надо тоже заказать. Спасибо за статью.
                                  +1

                                  Как ни странно, но unix timestamp. Только перевёрнутый и с часовым поясом.
                                  Например [83][a3][02][5d][08] это 5D02A383, что в десятичном виде будет 1560454019, а 08 — сдвиг +8 часов относительно UTC.
                                  404 FYI.
                                  Найти нужную характеристику просто — значение в начале меняется каждую секунду.


                                  Так и таймер обратного отсчёта можно реализовать (оно, правда, автоматически инкрементируется, но можно обновлять раз в полминуты).

                                    0
                                    О, ништяк. Как раз собирался этим вопросом заняться на выходных. Спасибо!

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое