Coder Social home page Coder Social logo

skysilver-lab / php-miio Goto Github PK

View Code? Open in Web Editor NEW
119.0 8.0 21.0 804 KB

Реализация взаимодействия с устройствами из экосистемы xiaomi по протоколу miIO

License: MIT License

PHP 100.00%
xiaomi miio php aqara mijia

php-miio's Introduction

php-miio

miIO - проприетарный шифрованный сетевой протокол Xiaomi, по которому взаимодействуют между собой wifi-устройства из экосистемы Xiaomi (Mi Home). Используется транспорт UDP и порт 54321. Ключ шифрования формируется на основе уникального токена, который есть у каждого устройства.

Функционал разделен и описан классами.

miio.class.php - класс для сетевого взаимодействия по протоколу miIO:

  • прием udp-пакетов из сокета
  • отправка udp-пакетов в сокет
  • процедура рукопожатия (handshake)
  • отправка сообщений устройству
  • прием ответов от устройства
  • поиск устройств (handshake-discovery)

mipacket.class.php - класс для работы с сетевыми udp-пакетами по протоколу miIO:

  • генерация ключа и вектора инициализации из токена
  • расшифровка
  • шифрование
  • парсинг udp-пакета
  • сборка udp-пакета

В качестве примера взаимодействия с устройствами написан скрипт для командной строки miio-cli.php. Принимаемые параметры:

  • --discover all - поиск устройств в локальной сети и вывод информации о них
  • --discover IP - проверка доступности конкретного устройства и вывод информации о нем
  • --info - получить информацию об устройстве (аналог --discover IP)
  • --sendcmd - отправить команду (в linux д.б. заключена в одинарные кавычки, в windows без них)
  • --decode - расшифровать пакет
  • --ip - IP-адрес устройства
  • --bindip - IP-адрес интерфейса сервера (не обязательно, если интерфейс один)
  • --token - токен устройства (не обязательно)
  • --debug - включает вывод отладочной информации
  • --help - справка по командам

Примеры:

php miio-cli.php --discover all --bindip 192.168.1.10
php miio-cli.php --discover 192.168.1.45 --debug
php miio-cli.php --ip 192.168.1.45 --info
php miio-cli.php --ip 192.168.1.45 --sendcmd '{"method":"toggle",,"params":[],"id":1}'
php miio-cli.php --ip 192.168.1.47 --sendcmd '{"id":1,"method":"get_prop","params":["power"]}'
php miio-cli.php --token b31c928032e6a4afc898c5c8768a518f --decode 2131004000000000035afe...bea030

Описание протокола miIO

1. Общие положения

miIO - проприетарный шифрованный сетевой протокол Xiaomi, по которому взаимодействуют wifi-устройства из экосистемы Xiaomi и приложение Mihome на смартфоне. В качествет транспорта используется UDP и порт 54321. Содержимое пакетов шифруется. Для контроля корректности принимаемых пакетов используется контрольная сумма на основе алгоритма MD5.

Данный протокол используется только при взаимодействии в пределах локальной сети! Взаимодействие между устройствами, приложением Mihome и облаком Xiaomi осуществляется по другому протоколу, расшифровать который пока никому не удалось.

2. Структура пакета

В протоколе miIO различают два типа пакетов - основной и hello-пакет. Hello-пакет применяется для поиска устройств в сети путем его широковещательной рассылки, либо для начала сессии с конкретным устройством. Для отправки устройству непосредственно команд используется основной пакет.

Пакет формируется из данных в hex-формате, состоит из заголовка (header) и полезной нагрузки (data).

Структура полей пакета приведена на схеме: Пакет miIO

  • Magic - "магическое" число, всегда равно 0х2131 (2 байта).
  • Length - длина пакета в байтах(заголовок+данные) (2 байта).
  • Unknown - поле неизвестного назначения. Всегда заполнено нулями 0х00000000, а у hello-пакета 0хFFFFFFFF (4 байта).
  • Device type - тип устройства (2 байта).
  • Device ID - идентификатор устройства (2 байта).
  • Time stamp - временная отметка, время работы устройства в секундах (4 байта).
  • Checksum - контрольная сумма всего пакета по алгоритму MD5. Перед расчетом КС это поле временно заполняется нулями (16 байт).
  • Data - полезная нагрузка произвольной длины - зашифрованные данные, отправляемые устройству. В hello-пакете это поле отсутствует.

В hello-пакете все поля, кроме Magic и Length, принимают значение 0хFF. Пакет miIO Hello

В особом случае, при ответе на hello-пакет, поле Checksum будет содержать 128-битное уникальное значение токена устройства. Это правило всегда актуально для новых, еще не привязанных к wifi устройств. В остальных случаях все зависит от прошивки устройства.

3. Сессия

Любое взаимодействие клиента и устройства начинается с "рукопожатия" (handshake). Клиент отправляет hello-пакет устройству и ждет от него ответ. Устройство в ответном пакете (длиной также 32 байта) отправляет свой тип, идентификатор, время работы в секундах и токен (либо нули вместо него). На основе полученных данных клиент формирует основной пакет с зашифрованной командой и отправляет устройству. Получив и выполнив команду от клиента, устройство отправляет ответный пакет с результатом выполнения принятой команды либо с ошибкой ее выполнения.

Процедура "рукопожатия" также используется для поиска устройств в локальной сети (discover). При этом hello-пакет отправляется не на конкретный IP, а на широковещательный адрес сегмента сети. Таким образом hello-пакеты получают все устройства, находящиеся в этом сегменте сети, и соответственно сообщают обратно клиенту о своем существовании.

4. Шифрование

Для шифрования отправляемых данных используется симметричный алгоритм шифрование AES128 в режиме CBC. 128-битные ключ шифрования (Key) и вектор инициализации (IV) формируются из уникального токена устройства по следующим формулам:

Key = MD5(Token);
IV = MD5(Token+IV);

Перед шифрованием необходимо выполнить процедуру дополнения данных PKCS#7 padding, а после расшифровки - обратную процедуру.

5. Формат команд (api)

Команды, отправляемые устройству и принимаемые от него, представлены в формате JSON.

Запрос --> {"id":1,"method":"get_prop","params":["power"]}
Ответ <-- {"id":1,"result":["ok"]}

Основные поля - это:

  • id - идентификатор запроса. Его значение не является обязательным для большинства, поэтому можно всегда выставлять равным 1. Но может быть полезен, когда одному и тому же устройству одновременно отправляются команды с разных клиентов. Для некоторых устройств (например, пылесос) данный параметр должен уникальным при каждом запросе.
  • method - метод, действие. Возможные варианты зависят от конкретного устройства, но есть и общие для всех.
  • params - массив свойств, параметров. Возможные варианты зависят от конкретного устройства.

Токен miIO-устройства

Токен - это уникальная 32-ухзначная последовательность символов, используемая для формирования ключа шифрования. Наличие и знание токена - это обязательное условие успешного управления miIO-устройством (далее устройство).

В целом процедура добавления нового устройства в приложение Mihome выглядит так:

  1. Включаем новое устройство в сеть. Оно создает свою открытую точку доступа.
  2. Приложение Mihome производит поиск новых wifi-сетей, и если находит, то предлагает добавить устройство.
  3. При добавлении телефон подключается к точке доступа, созданной устройством.
  4. Mihome посылает hello-пакет устройству.
  5. Устройство, получив hello-пакет, отправляет ответ на него, в котором содержится токен.
  6. Mihome получает ответ, сохраняет токен в свою базу данных и отправляет устройству команду на подключение к wifi-сети и пароль от нее.
  7. Устройство перезагружается и цепляется к целевой wifi-сети. Телефон также переключается обратно на основную точку доступа.
  8. Mihome и устройство обмениваются пакетами по протоколу miIO, зашифрованного на основе полученного ранее токена.

После привязки устройства к Mihome устройства в большинтсве случаев перестают транслировать свой токен в ответ на hello-пакеты. Это зависит от логики, зашитой в прошивку устройства, и версии этой прошивки. Одни устройства всегда, на любой версии прошивки, открыто отдают свой токен. Вторые отдают токен только до определенной версии прошивки, а после обновления перестают. Ну а третьи сообщают свой токен только в режиме инициализации, т.е. до привязки к Mihome и подключения к wifi-сети.

Исходя из вышеуказанной процедуры, можно рассмотреть несколько вариантов получения токена устройства.

  1. Произвести поиск устройств в сети с помощью handshake discover.
  2. Извлечь токены из базы данных или кеш-файлов приложения Mihome на смартфоне.
  3. Сбросить устройство (или удалить из Mihome) и получить токен в режиме инициализации устройства.

Рассмотрим эти варианты подробнее.

1. Поиск устройств в сети (handshake discover)

Для поиска miIO-устройств необходимо на компьютере с установленным PHP и подключенном к локальной сети выполнить в консоли команду:

php miio-cli.php --discover all

Результатом команды будет список найденных устройств и в случае успеха их токенов. Если устройство не транслирует свой токен, то значение будет заполнено нулями, и в таком случае выяснять токен придется другими способами.

discoverall

Можно не опрашивать все устройства в сети, а отправить запрос адресно на конкретный IP:

php miio-cli.php --discover 192.168.1.47 --debug

Помимо консольной утилиты miio-cli.php можно воспользоваться кросс-платформенным приложением Packet Sender или аналогичными утилитами для смартфонов (например Packet Handler для андроида). В качестве отправляемого сообщения указать 21310020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF. Остальные настройки наглядно представлены на скриншотах.

packsender1 packethandler1 packethandler2

2. База данных и кеш-файлы Mihome

На андроид смартфоне с рутом в папке /data/data/com.xiaomi.smarthome/databases/ нужно найти файл базы данных приложения miio2.db и скопировать его в любую пользовательскую папку. Затем с помощью приложения aSQLiteManager открыть эту базу и в таблице devicerecord найти столбец token. В нем и будут токены всех устройств.

Чтобы скопировать файл базы данных на нерутованных смартфонах, нужно включить режим USB-отладки и подключиться по ADB. Затем в консоли ADB сделать резервную копию приложения Mihome командой

adb backup -noapk com.xiaomi.smarthome -f backup.ab

Полученный архив распаковывается java-утилитой ADB Backup Extractor

java.exe -jar abe.jar unpack backup.ab backup.tar

Файл backup.tar открываем обычным архиватором (7-zip) и ищем там базу miio2.db. Для просмотра базы на ПК можно воспользоваться SQLite browser.

miio2db sqlite

Аналогичную процедуру можно выполнить с помощью утилиты MiToolkit. Суть та же, что и через ADB, только через windows-приложение, чтобы не ковыряться в консоли.

Условия:

  1. Установленные на ПК ADB-драйвера смартфона.
  2. Установленная на ПК Java.
  3. На смартфоне включена отладка через USB и разрешено подключение с ПК.
  4. И, разумеется, смартфон подключен по USB к ПК.

Скачиваем на ПК утилиту MiToolkit 1.5 и распаковываем архив. Запускаем MiToolkit.exe.

Переключаем на английский язык и нажимаем Extract Token.

mitoolkit_1.png

Появится окно с описанием процесса. В нем снова нажимаем Extract Token. mitoolkit_2.png

Через некоторое время на телефоне запустится приложение Mihome, а на ПК появится предупреждение, что ни в коем случае не ставить пароль на резервную копию приложения Mihome. Нажимаем ОК. mitoolkit_3.png

Далее на смартфоне появится сообщение о подтверждении создания резервной копии приложения Mihome. Оставляем поле ввода пароля пустым и тапаем Создать резервную копию данных. mitoolkit_6.png

После успешной архивации на ПК появится соответствующее сообщение. Нажимаем ОК и ждем результата. mitoolkit_4.png

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

Кроме того, на рутованных смартфонах токены также можно найти в файлах кеша /data/data/com.xiaomi.smarthome/cache/smrc4-cache. Например, через тот же ADB это выглядит так:

adb root
adb shell
cd /data/data/com.xiaomi.smarthome/cache/smrc4-cache
grep -nr token .

3. Сброс устройства и последующая инициализация

Здесь все то же самое, что и в первом варианте. За исключением того, что предварительно нужно подключиться к открытой точке доступа, которую создает не настроенное miIO-устройство, и выяснить какие IP-адреса выданы. miIOwifiAP

Ссылки

php-miio's People

Contributors

bravehurts2 avatar skysilver-lab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

php-miio's Issues

miio-cli.php smart_night_light_on

С лампой все отлично, но появилась мысль добавить к домотиксу ночник этой ламы. Нашел параметр enable_bl, но как не пробовал, приходит ответ "ок" но ночник в ней не включается. Я что-то делаю не так?

Device does not answer

Hi,
Thanks a lot for your great work.
I'm using php-miio master with domoticz to remote my Xiaomi Philips Bulb.
Everything worked great.
Sometimes my script was launched but I had no light. It was randomly not working.
Those days it turned to nightmare, bulb doesn't turn to on or sometimes. I thought it was due to domoticz update. But I tried with my Ubuntu to command my bulbs and I had no answer. With the MiHome app bulb does answer. So my scripts are launched but it does not always work. I don't know what to do.

Thanks

Устройство не доступно

$php miio-cli.php --token xxxxx --ip 192.168.1.12 --sendcmd '{"method":"get_channels","params":{"start":0}, "id": 3}}'
Устройство 192.168.1.12 не доступно или не отвечает.

один раз сработало

В то же время python-miio отрабатывает ОК

$miiocli device --ip 192.168.1.12 --token xxxx raw_command "get_channels" "{'start':0}"
Running command raw_command
WARNING:miio.device:Found an unsupported model 'lumi.gateway.v3' for class 'Device'. If this is working for you, please open an issue at https://github.com/rytilahti/python-miio/
{'chs': [{'id': 2048, 'type': 0, 'url': ''}]}

sending commands only successful in combination with mirobo

for some reason the command
php miio-cli.php --ip xx.yy.xx.xx --debug --sendcmd '{"id":1,"method":"find_me","params":[]}' --token aabbccdd...
is only successful if I send a command with mirobo first:
mirobo --token aabbccdd... --ip xx.yy.xx.xx status

otherwise i run into this line without data

return false;

do you have an idea whats going wrong?

thanks for sharing your work. I'm just searching how to encrypt the data packages php based correctly.

Ошибка mirobot-sample.php

Добрый день
API выдает ошибку, устройство Xiaomi Mi 1C
Статус не получен. Ошибка: {"id":15,"error":{"code":-9999,"message":"user ack timeout"},"exe_time":4002}

token

Добрый день, огромное спасибо Вам за подробное описание протокола mio.

Я пробую взаимодействовать с mio первый раз.
Мне нужно считывать показания датчика температуры.

Исходя из Вашего руководства в первую очередь необходимо получить token для устройств.
Вы предложили 3 способа.

Попробовал все 3 - но как то безуспешно :)

Исходные данные:
Есть Xiaomi gateway выпуска 2018 года.
Подключил, привязал к Mi Home

Запустил php miio-cli.php --discover all
Почему то даже наличие gateway не найдено , не говоря о токене

Использовал метод 2, поставил adb, сделал архив, открыл базу miio2.db
gateway присутствует, другие устройства присутствуют, но token везде пустой.

Сбросил gateway (длительное нажатие на кнопку порядка 10 сек)
Подключился к его ap с компьютера , запустил php miio-cli.php --discover all - не найдено ни одного устройства.

Датчик температуры без gateway не прочитать .
Посоветуйте пожалуйста - что можно еще попробовать?

Получение токена через облако

Предлагаю портировать к себе такой способ получения токена через api и учетную запись на xiaomi:

import base64
import hashlib
import hmac
import json
import os
import requests

from Crypto.Hash import MD5, SHA256

class XiaomiCloudConnector:

    def __init__(self, username, password):
        self._username = username
        self._password = password
        self._session = requests.session()
        self._ssecurity = None
        self._userId = None
        self._location = None
        self._serviceToken = None

    def login_step_2(self):
        url = "https://account.xiaomi.com/pass/serviceLoginAuth2"
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        fields = {
            "sid": "xiaomiio",
            "hash": (MD5.new(str.encode(self._password)).hexdigest() + "").upper(),
            "user": self._username,
            "_json": "true"
        }
        response = self._session.post(url, headers=headers, params=fields)
        valid = response.status_code == 200 and "ssecurity" in self.to_json(response.text)
        if valid:
            json_resp = self.to_json(response.text)
            self._ssecurity = json_resp["ssecurity"]
            self._userId = json_resp["userId"]
            self._location = json_resp["location"]

        return valid

    def login_step_3(self):
        headers = {
            #"User-Agent": self._agent,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        response = self._session.get(self._location, headers=headers)
        if response.status_code == 200:
            self._serviceToken = response.cookies.get("serviceToken")
        return response.status_code == 200

    def login(self):
        if self.login_step_2():
            if self.login_step_3():
                return True
            else:
                print("Unable to get service token.")
        else:
            print("Invalid login or password.")

        return False

    def get_devices(self, country):
        url = self.get_api_url(country) + "/home/device_list"
        params = {
            "data": '{"getVirtualModel":false,"getHuamiDevices":0}'
        }
        return self.execute_api_call(url, params)

    def execute_api_call(self, url, params):
        headers = {
            "Accept-Encoding": "gzip",
            "Content-Type": "application/x-www-form-urlencoded",
            "x-xiaomi-protocal-flag-cli": "PROTOCAL-HTTP2"
        }
        cookies = {
            "userId": str(self._userId),
            "serviceToken": str(self._serviceToken),
        }

        nonce = self.generate_nonce()
        signed_nonce = self.signed_nonce(nonce)
        signature = self.generate_signature(url.replace("/app", ""), signed_nonce, nonce, params)
        fields = {
            "signature": signature,
            "_nonce": nonce,
            "data": params["data"]
        }
        response = self._session.post(url, headers=headers, cookies=cookies, params=fields)
        if response.status_code == 200:
            return response.json()
        return None

    def get_api_url(self, country):
        return "https://" + ("" if country == "cn" else (country + ".")) + "api.io.mi.com/app"

    def signed_nonce(self, nonce):
        hash_object = SHA256.new()
        hash_object.update( base64.b64decode(self._ssecurity) + base64.b64decode(nonce) )
        return base64.b64encode(hash_object.digest()).decode('utf-8')

    @staticmethod
    def generate_nonce():
        nonce_bytes = os.urandom(12)
        return base64.b64encode(nonce_bytes).decode()

    @staticmethod
    def generate_signature(url, signed_nonce, nonce, params):
        signature_params = [url.split("com")[1], signed_nonce, nonce]
        for k, v in params.items():
            signature_params.append(f"{k}={v}")
        signature_string = "&".join(signature_params)
        signature = hmac.new(base64.b64decode(signed_nonce), msg=signature_string.encode(), digestmod=hashlib.sha256)
        return base64.b64encode(signature.digest()).decode()

    @staticmethod
    def to_json(response_text):
        return json.loads(response_text.replace("&&&START&&&", ""))


print("Username (email or user ID):")
username = input()

print("Password:")
password = input()

print("Country (one of: ru, us, tw, sg, cn, de) Leave empty to check all available:")
country = input()

while country not in ["", "ru", "us", "tw", "sg", "cn", "de"]:
    print("Invalid country provided. Valid values: ru, us, tw, sg, cn, de")
    print("Country:")
    country = input()

print()
countries = ["cn", "de", "us", "ru", "tw", "sg"]
if not country == "":
    countries = [country]

connector = XiaomiCloudConnector(username, password)
print("Logging in...")
logged = connector.login()
if logged:
    print("Logged in.")
    print()
    for current_country in countries:
        devices = connector.get_devices(current_country)
        if devices is not None:
            if len(devices["result"]["list"]) == 0:
                print(f"No devices found for country \"{current_country}\".")
                continue
            print(f"Devices found for country \"{current_country}\":")
            for device in devices["result"]["list"]:
                print("   ---------")
                if "name" in device:
                    print("   NAME:  " + device["name"])
                if "did" in device:
                    print("   ID:    " + device["did"])
                if "localip" in device:
                    print("   IP:    " + device["localip"])
                if "token" in device:
                    print("   TOKEN: " + device["token"])
                if "model" in device:
                    print("   MODEL: " + device["model"])
            print("   ---------")
            print()
        else:
            print("Unable to get devices.")
else:
    print("Unable to log in.")

Полная более избыточная версия исходная - https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/blob/master/token_extractor.py

mirobot clean zone

Спасибо большое за ваш трут!
Сделайте пожалуйста возможность просто отправлять команды на уборку нужной зоны.
Например:

$mirobot->zone(29500,21000,33200,25000,1)

В строчке указаны координаты зоны уборки и количество раз.
Буду очень рад за помощь, спасибо.

https://github.com/marcelrv/XiaomiRobotVacuumProtocol/blob/master/app_zoned_clean.md

https://hackernoon.com/how-i-set-up-room-cleaning-automation-with-google-home-home-assistant-and-xiaomi-vacuum-cleaner-9149e0267e6d

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.