├── LICENSE ├── README.md ├── devices ├── mirobot.class.php ├── philipsbulb.class.php └── philipseyecare2.class.php ├── img ├── discoverall.png ├── miIO_hello.png ├── miIO_пакет.png ├── miio2db.png ├── mitoolkit_1.png ├── mitoolkit_2.png ├── mitoolkit_3.png ├── mitoolkit_4.png ├── mitoolkit_5.png ├── mitoolkit_6.png ├── packethandler1.png ├── packethandler2.png ├── packsender1.png ├── sqlite.png └── wifi_ap.png ├── miio-cli.php ├── miio-sample.php ├── miio.class.php ├── mipacket.class.php ├── mirobot-sample.php └── philipsbulb-sample.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 skysilver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-miio 2 | 3 | **miIO** - проприетарный шифрованный сетевой протокол Xiaomi, по которому взаимодействуют между собой wifi-устройства из экосистемы Xiaomi (Mi Home). Используется транспорт UDP и порт 54321. Ключ шифрования формируется на основе уникального токена, который есть у каждого устройства. 4 | 5 | Функционал разделен и описан классами. 6 | 7 | `miio.class.php` - класс для сетевого взаимодействия по протоколу **miIO**: 8 | * прием udp-пакетов из сокета 9 | * отправка udp-пакетов в сокет 10 | * процедура рукопожатия (handshake) 11 | * отправка сообщений устройству 12 | * прием ответов от устройства 13 | * поиск устройств (handshake-discovery) 14 | 15 | `mipacket.class.php` - класс для работы с сетевыми udp-пакетами по протоколу **miIO**: 16 | * генерация ключа и вектора инициализации из токена 17 | * расшифровка 18 | * шифрование 19 | * парсинг udp-пакета 20 | * сборка udp-пакета 21 | 22 | 23 | В качестве примера взаимодействия с устройствами написан скрипт для командной строки `miio-cli.php`. 24 | Принимаемые параметры: 25 | * `--discover all` - поиск устройств в локальной сети и вывод информации о них 26 | * `--discover IP` - проверка доступности конкретного устройства и вывод информации о нем 27 | * `--info` - получить информацию об устройстве (аналог --discover IP) 28 | * `--sendcmd` - отправить команду (в linux д.б. заключена в одинарные кавычки, в windows без них) 29 | * `--decode` - расшифровать пакет 30 | * `--ip` - IP-адрес устройства 31 | * `--bindip` - IP-адрес интерфейса сервера (не обязательно, если интерфейс один) 32 | * `--token` - токен устройства (не обязательно) 33 | * `--debug` - включает вывод отладочной информации 34 | * `--help` - справка по командам 35 | 36 | Примеры: 37 | ```php miio-cli.php --discover all 38 | php miio-cli.php --discover all --bindip 192.168.1.10 39 | php miio-cli.php --discover 192.168.1.45 --debug 40 | php miio-cli.php --ip 192.168.1.45 --info 41 | php miio-cli.php --ip 192.168.1.45 --sendcmd '{"method":"toggle",,"params":[],"id":1}' 42 | php miio-cli.php --ip 192.168.1.47 --sendcmd '{"id":1,"method":"get_prop","params":["power"]}' 43 | php miio-cli.php --token b31c928032e6a4afc898c5c8768a518f --decode 2131004000000000035afe...bea030 44 | ``` 45 | 46 | ## Описание протокола miIO 47 | 48 | ### 1. Общие положения 49 | **miIO** - проприетарный шифрованный сетевой протокол Xiaomi, по которому взаимодействуют wifi-устройства из экосистемы Xiaomi и приложение Mihome на смартфоне. В качествет транспорта используется UDP и порт 54321. Содержимое пакетов шифруется. Для контроля корректности принимаемых пакетов используется контрольная сумма на основе алгоритма MD5. 50 | 51 | Данный протокол используется только при взаимодействии в пределах локальной сети! Взаимодействие между устройствами, приложением Mihome и облаком Xiaomi осуществляется по другому протоколу, расшифровать который пока никому не удалось. 52 | 53 | ### 2. Структура пакета 54 | В протоколе **miIO** различают два типа пакетов - основной и hello-пакет. Hello-пакет применяется для поиска устройств в сети путем его широковещательной рассылки, либо для начала сессии с конкретным устройством. Для отправки устройству непосредственно команд используется основной пакет. 55 | 56 | Пакет формируется из данных в hex-формате, состоит из заголовка (header) и полезной нагрузки (data). 57 | 58 | Структура полей пакета приведена на схеме: 59 | ![Пакет miIO](img/miIO_пакет.png) 60 | 61 | * `Magic` - "магическое" число, всегда равно `0х2131` (2 байта). 62 | * `Length` - длина пакета в байтах(заголовок+данные) (2 байта). 63 | * `Unknown` - поле неизвестного назначения. Всегда заполнено нулями `0х00000000`, а у hello-пакета `0хFFFFFFFF` (4 байта). 64 | * `Device type` - тип устройства (2 байта). 65 | * `Device ID` - идентификатор устройства (2 байта). 66 | * `Time stamp` - временная отметка, время работы устройства в секундах (4 байта). 67 | * `Checksum` - контрольная сумма всего пакета по алгоритму MD5. Перед расчетом КС это поле временно заполняется нулями (16 байт). 68 | * `Data` - полезная нагрузка произвольной длины - зашифрованные данные, отправляемые устройству. В hello-пакете это поле отсутствует. 69 | 70 | В hello-пакете все поля, кроме `Magic` и `Length`, принимают значение `0хFF`. 71 | ![Пакет miIO Hello](img/miIO_hello.png) 72 | 73 | В особом случае, при ответе на hello-пакет, поле `Checksum` будет содержать 128-битное уникальное значение токена устройства. Это правило всегда актуально для новых, еще не привязанных к wifi устройств. В остальных случаях все зависит от прошивки устройства. 74 | 75 | ### 3. Сессия 76 | Любое взаимодействие клиента и устройства начинается с "рукопожатия" (handshake). Клиент отправляет hello-пакет устройству и ждет от него ответ. Устройство в ответном пакете (длиной также 32 байта) отправляет свой тип, идентификатор, время работы в секундах и токен (либо нули вместо него). На основе полученных данных клиент формирует основной пакет с зашифрованной командой и отправляет устройству. Получив и выполнив команду от клиента, устройство отправляет ответный пакет с результатом выполнения принятой команды либо с ошибкой ее выполнения. 77 | 78 | Процедура "рукопожатия" также используется для поиска устройств в локальной сети (discover). При этом hello-пакет отправляется не на конкретный IP, а на широковещательный адрес сегмента сети. Таким образом hello-пакеты получают все устройства, находящиеся в этом сегменте сети, и соответственно сообщают обратно клиенту о своем существовании. 79 | 80 | ### 4. Шифрование 81 | Для шифрования отправляемых данных используется симметричный алгоритм шифрование AES128 в режиме CBC. 128-битные ключ шифрования (Key) и вектор инициализации (IV) формируются из уникального токена устройства по следующим формулам: 82 | ``` 83 | Key = MD5(Token); 84 | IV = MD5(Token+IV); 85 | ``` 86 | 87 | Перед шифрованием необходимо выполнить процедуру дополнения данных `PKCS#7 padding`, а после расшифровки - обратную процедуру. 88 | 89 | ### 5. Формат команд (api) 90 | Команды, отправляемые устройству и принимаемые от него, представлены в формате JSON. 91 | ``` 92 | Запрос --> {"id":1,"method":"get_prop","params":["power"]} 93 | Ответ <-- {"id":1,"result":["ok"]} 94 | ``` 95 | Основные поля - это: 96 | * `id` - идентификатор запроса. Его значение не является обязательным для большинства, поэтому можно всегда выставлять равным 1. Но может быть полезен, когда одному и тому же устройству одновременно отправляются команды с разных клиентов. Для некоторых устройств (например, пылесос) данный параметр должен уникальным при каждом запросе. 97 | * `method` - метод, действие. Возможные варианты зависят от конкретного устройства, но есть и общие для всех. 98 | * `params` - массив свойств, параметров. Возможные варианты зависят от конкретного устройства. 99 | 100 | ## Токен miIO-устройства 101 | 102 | **Токен** - это уникальная 32-ухзначная последовательность символов, используемая для формирования ключа шифрования. 103 | Наличие и знание токена - это обязательное условие успешного управления miIO-устройством (далее устройство). 104 | 105 | В целом процедура добавления нового устройства в приложение `Mihome` выглядит так: 106 | 107 | 1. Включаем новое устройство в сеть. Оно создает свою открытую точку доступа. 108 | 2. Приложение Mihome производит поиск новых wifi-сетей, и если находит, то предлагает добавить устройство. 109 | 3. При добавлении телефон подключается к точке доступа, созданной устройством. 110 | 4. Mihome посылает hello-пакет устройству. 111 | 5. Устройство, получив hello-пакет, отправляет ответ на него, в котором содержится токен. 112 | 6. Mihome получает ответ, сохраняет токен в свою базу данных и отправляет устройству команду на подключение к wifi-сети и пароль от нее. 113 | 7. Устройство перезагружается и цепляется к целевой wifi-сети. Телефон также переключается обратно на основную точку доступа. 114 | 8. Mihome и устройство обмениваются пакетами по протоколу **miIO**, зашифрованного на основе полученного ранее токена. 115 | 116 | После привязки устройства к Mihome устройства в большинтсве случаев перестают транслировать свой токен в ответ на hello-пакеты. Это зависит от логики, зашитой в прошивку устройства, и версии этой прошивки. Одни устройства всегда, на любой версии прошивки, открыто отдают свой токен. Вторые отдают токен только до определенной версии прошивки, а после обновления перестают. Ну а третьи сообщают свой токен только в режиме инициализации, т.е. до привязки к Mihome и подключения к wifi-сети. 117 | 118 | Исходя из вышеуказанной процедуры, можно рассмотреть несколько вариантов получения токена устройства. 119 | 120 | 1. Произвести поиск устройств в сети с помощью `handshake discover`. 121 | 2. Извлечь токены из базы данных или кеш-файлов приложения Mihome на смартфоне. 122 | 3. Сбросить устройство (или удалить из Mihome) и получить токен в режиме инициализации устройства. 123 | 124 | Рассмотрим эти варианты подробнее. 125 | 126 | ### 1. Поиск устройств в сети (handshake discover) 127 | Для поиска miIO-устройств необходимо на компьютере с установленным PHP и подключенном к локальной сети выполнить в консоли команду: 128 | ``` 129 | php miio-cli.php --discover all 130 | ``` 131 | Результатом команды будет список найденных устройств и в случае успеха их токенов. Если устройство не транслирует свой токен, то значение будет заполнено нулями, и в таком случае выяснять токен придется другими способами. 132 | 133 | ![discoverall](img/discoverall.png) 134 | 135 | Можно не опрашивать все устройства в сети, а отправить запрос адресно на конкретный IP: 136 | ``` 137 | php miio-cli.php --discover 192.168.1.47 --debug 138 | ``` 139 | Помимо консольной утилиты `miio-cli.php` можно воспользоваться кросс-платформенным приложением **Packet Sender** или аналогичными утилитами для смартфонов (например **Packet Handler** для андроида). В качестве отправляемого сообщения указать `21310020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`. Остальные настройки наглядно представлены на скриншотах. 140 | 141 | ![packsender1](img/packsender1.png) 142 | ![packethandler1](img/packethandler1.png) 143 | ![packethandler2](img/packethandler2.png) 144 | 145 | ### 2. База данных и кеш-файлы Mihome 146 | На андроид смартфоне с рутом в папке `/data/data/com.xiaomi.smarthome/databases/` нужно найти файл базы данных приложения **miio2.db** и скопировать его в любую пользовательскую папку. Затем с помощью приложения **aSQLiteManager** открыть эту базу и в таблице **devicerecord** найти столбец **token**. В нем и будут токены всех устройств. 147 | 148 | Чтобы скопировать файл базы данных на нерутованных смартфонах, нужно включить режим USB-отладки и подключиться по ADB. Затем в консоли ADB сделать резервную копию приложения Mihome командой 149 | ``` 150 | adb backup -noapk com.xiaomi.smarthome -f backup.ab 151 | ``` 152 | Полученный архив распаковывается java-утилитой **ADB Backup Extractor** 153 | ``` 154 | java.exe -jar abe.jar unpack backup.ab backup.tar 155 | ``` 156 | Файл `backup.tar` открываем обычным архиватором (**7-zip**) и ищем там базу `miio2.db`. Для просмотра базы на ПК можно воспользоваться **SQLite browser**. 157 | 158 | ![miio2db](img/miio2db.png) 159 | ![sqlite](img/sqlite.png) 160 | 161 | Аналогичную процедуру можно выполнить с помощью утилиты **MiToolkit**. Суть та же, что и через ADB, только через windows-приложение, чтобы не ковыряться в консоли. 162 | 163 | Условия: 164 | 1. Установленные на ПК ADB-драйвера смартфона. 165 | 2. Установленная на ПК Java. 166 | 3. На смартфоне включена отладка через USB и разрешено подключение с ПК. 167 | 4. И, разумеется, смартфон подключен по USB к ПК. 168 | 169 | Скачиваем на ПК утилиту [MiToolkit 1.5](https://github.com/ultrara1n/MiToolkit/releases/download/1.5/MiToolkit.1.5.zip) и распаковываем архив. Запускаем `MiToolkit.exe`. 170 | 171 | Переключаем на английский язык и нажимаем **Extract Token**. 172 | 173 | ![mitoolkit_1.png](img/mitoolkit_1.png) 174 | 175 | Появится окно с описанием процесса. В нем снова нажимаем **Extract Token**. 176 | ![mitoolkit_2.png](img/mitoolkit_2.png) 177 | 178 | Через некоторое время на телефоне запустится приложение **Mihome**, а на ПК появится предупреждение, что ни в коем случае не ставить пароль на резервную копию приложения Mihome. Нажимаем **ОК**. 179 | ![mitoolkit_3.png](img/mitoolkit_3.png) 180 | 181 | Далее на смартфоне появится сообщение о подтверждении создания резервной копии приложения **Mihome**. Оставляем поле ввода пароля пустым и тапаем **Создать резервную копию данных**. 182 | ![mitoolkit_6.png](img/mitoolkit_6.png) 183 | 184 | После успешной архивации на ПК появится соответствующее сообщение. Нажимаем **ОК** и ждем результата. 185 | ![mitoolkit_4.png](img/mitoolkit_4.png) 186 | 187 | По окончанию экспорта устройств из базы приложения в основном окне будет заполнено соответствующее поле. Если устройство много, то нужно прокрутить список вниз. Полоса прокрутки при этом не отображается. 188 | ![mitoolkit_5.png](img/mitoolkit_5.png) 189 | 190 | 191 | Кроме того, на рутованных смартфонах токены также можно найти в файлах кеша `/data/data/com.xiaomi.smarthome/cache/smrc4-cache`. Например, через тот же ADB это выглядит так: 192 | ``` 193 | adb root 194 | adb shell 195 | cd /data/data/com.xiaomi.smarthome/cache/smrc4-cache 196 | grep -nr token . 197 | ``` 198 | 199 | ### 3. Сброс устройства и последующая инициализация 200 | Здесь все то же самое, что и в первом варианте. За исключением того, что предварительно нужно подключиться к открытой точке доступа, которую создает не настроенное miIO-устройство, и выяснить какие IP-адреса выданы. 201 | ![miIOwifiAP](img/wifi_ap.png) 202 | 203 | 204 | ### Ссылки 205 | * https://github.com/aholstenson/miio 206 | * https://github.com/rytilahti/python-miio 207 | * https://github.com/OpenMiHome/mihome-binary-protocol 208 | * https://github.com/marcelrv/XiaomiRobotVacuumProtocol 209 | -------------------------------------------------------------------------------- /devices/mirobot.class.php: -------------------------------------------------------------------------------- 1 | 'Unknown', 23 | '1' => 'Initiating', 24 | '2' => 'Sleeping', 25 | '3' => 'Waiting', 26 | '4' => 'Unknown', 27 | '5' => 'Cleaning', 28 | '6' => 'Back to home', 29 | '7' => 'Unknown', 30 | '8' => 'Charging', 31 | '9' => 'Charging Error', 32 | '10' => 'Pause', 33 | '11' => 'Spot Cleaning', 34 | '12' => 'In Error', 35 | '13' => 'Shutting down', 36 | '14' => 'Updating', 37 | '15' => 'Docking', 38 | '100' => 'Full'); 39 | 40 | private $error_codes = array('0' => 'No error', 41 | '1' => 'Laser distance sensor error', 42 | '2' => 'Collision sensor error', 43 | '3' => 'Wheels on top of void, move robot', 44 | '4' => 'Clean hovering sensors, move robot', 45 | '5' => 'Clean main brush', 46 | '6' => 'Clean side brush', 47 | '7' => 'Main wheel stuck', 48 | '8' => 'Device stuck, clean area', 49 | '9' => 'Dust collector missing', 50 | '10' => 'Clean filter', 51 | '11' => 'Stuck in magnetic barrier', 52 | '12' => 'Low battery', 53 | '13' => 'Charging fault', 54 | '14' => 'Battery fault', 55 | '15' => 'Wall sensors dirty, wipe them', 56 | '16' => 'Place me on flat surface', 57 | '17' => 'Side brushes problem, reboot me', 58 | '18' => 'Suction fan problem', 59 | '19' => 'Unpowered charging station'); 60 | 61 | public function __construct($ip = NULL, $bind_ip = NULL, $token = NULL, $debug = false) { 62 | 63 | $this->ip = $ip; 64 | $this->token = $token; 65 | $this->debug = $debug; 66 | 67 | if ($bind_ip != NULL) $this->bind_ip = $bind_ip; 68 | else $this->bind_ip = '0.0.0.0'; 69 | 70 | $this->dev = new miIO($this->ip, $this->bind_ip, $this->token, $this->debug); 71 | 72 | } 73 | 74 | /* 75 | Активирует авто-формирование уникальных ID сообщений с их сохранением в файл id.json 76 | */ 77 | 78 | public function enableAutoMsgID() { 79 | 80 | $this->dev->useAutoMsgID = true; 81 | 82 | } 83 | 84 | /* 85 | Деактивирует авто-формирование уникальных ID сообщений с их сохранением в файл id.json 86 | ID сообщений необходимо передавать в виде аргумента при каждой отправке команды, 87 | либо не указывать вообще, тогда ID будет 1 для всех сообщений. 88 | */ 89 | 90 | public function disableAutoMsgID() { 91 | 92 | $this->dev->useAutoMsgID = false; 93 | 94 | } 95 | 96 | /* 97 | Получить расширенные сведения miIO 98 | */ 99 | 100 | public function getInfo($msg_id = 1) { 101 | 102 | if ($this->dev->getInfo($msg_id)) return $this->dev->data; 103 | else return false; 104 | 105 | } 106 | 107 | /* 108 | Получить текущий статус: 109 | state - код состояния 110 | state_text - состояние 111 | battery - заряд батареи 112 | fan_power - мощность турбины 113 | error_code - код ошибки 114 | error_text - описание ошибки 115 | clean_area - площадь уборки, кв. см. 116 | clean_time - время уборки, сек. 117 | dnd_enabled - режим "не беспокоить" 118 | in_cleaning - в процессе уборки или нет 119 | map_present - есть карта или нет 120 | msg_ver - версия команд 121 | msg_seq - счетчик команд 122 | */ 123 | 124 | public function getStatus($msg_id = 1) { 125 | 126 | $result = $this->dev->msgSendRcv('get_status', '[]', $msg_id); 127 | 128 | if ($result) { 129 | if ($this->dev->data != '') { 130 | $res = json_decode($this->dev->data, true); 131 | if (isset($res['result'][0])) { 132 | foreach($res['result'][0] as $key => $value) { 133 | $this->status[$key] = $value; 134 | if ($key == 'state') { 135 | if (array_key_exists($value, $this->state_codes)) $this->status['state_text'] = $this->state_codes[$value]; 136 | } 137 | if ($key == 'error_code') { 138 | if (array_key_exists($value, $this->error_codes)) $this->status['error_text'] = $this->error_codes[$value]; 139 | } 140 | } 141 | return true; 142 | } else if (isset($res['error'])) { 143 | $this->error = $this->dev->data; 144 | return false; 145 | } 146 | } else { 147 | $this->error = 'Нет данных'; 148 | return false; 149 | } 150 | } else { 151 | $this->error = 'Ответ не получен'; 152 | return false; 153 | } 154 | 155 | } 156 | 157 | /* 158 | Начать уборку 159 | */ 160 | 161 | public function start($msg_id = 1) { 162 | 163 | $result = $this->dev->msgSendRcv('app_start', '[]', $msg_id); 164 | return $this->verify($result); 165 | 166 | } 167 | 168 | /* 169 | Завершить уборку 170 | */ 171 | 172 | public function stop($msg_id = 1) { 173 | 174 | $result = $this->dev->msgSendRcv('app_stop', '[]', $msg_id); 175 | return $this->verify($result); 176 | 177 | } 178 | 179 | /* 180 | Приостановить уборку 181 | */ 182 | 183 | public function pause($msg_id = 1) { 184 | 185 | $result = $this->dev->msgSendRcv('app_pause', '[]', $msg_id); 186 | return $this->verify($result); 187 | 188 | } 189 | 190 | /* 191 | Убрать вокруг себя 192 | */ 193 | 194 | public function cleanSpot($msg_id = 1) { 195 | 196 | $result = $this->dev->msgSendRcv('app_spot', '[]', $msg_id); 197 | return $this->verify($result); 198 | 199 | } 200 | 201 | /* 202 | Вернуться на базу и встать на зарядку 203 | */ 204 | 205 | public function charge($msg_id = 1) { 206 | 207 | $result = $this->dev->msgSendRcv('app_charge', '[]', $msg_id); 208 | return $this->verify($result); 209 | 210 | } 211 | 212 | /* 213 | Поиск пылесоса 214 | */ 215 | 216 | public function findMe($msg_id = 1) { 217 | 218 | $result = $this->dev->msgSendRcv('find_me', '[]', $msg_id); 219 | return $this->verify($result); 220 | 221 | } 222 | 223 | /* 224 | Зональная уборка 225 | */ 226 | 227 | public function zoned_clean(int $x1_coord, int $y1_coord, int $x2_coord, int $y2_coord, int $count, $msg_id = 1) { 228 | 229 | $result = $this->dev->msgSendRcv('app_zoned_clean', "[[$x1_coord,$y1_coord,$x2_coord,$y2_coord,$count]]", $msg_id); 230 | return $this->verify($result); 231 | 232 | } 233 | 234 | /* 235 | Проверка ответа 236 | */ 237 | 238 | public function verify ($result) { 239 | 240 | if ($result) { 241 | if ($this->dev->data != '') { 242 | $res = json_decode($this->dev->data); 243 | if($res instanceof \stdClass && property_exists($res, 'result')){ 244 | if(is_array($res->{'result'}) && in_array('ok', $res->{'result'})) { 245 | return true; 246 | } 247 | elseif (isset($res->{'result'}) && ($res->{'result'} == 0)) return true; 248 | } else { 249 | $this->error = $this->dev->data; 250 | return false; 251 | } 252 | } else { 253 | $this->error = 'Нет данных'; 254 | return false; 255 | } 256 | } else { 257 | $this->error = 'Ответ не получен'; 258 | return false; 259 | } 260 | 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /devices/philipsbulb.class.php: -------------------------------------------------------------------------------- 1 | '', 19 | 'bright' => '', 20 | 'cct' => '', 21 | 'snm' => '', 22 | 'dv' => ''); 23 | 24 | public $dev = NULL; 25 | 26 | public function __construct($ip = NULL, $bind_ip = NULL, $token = NULL, $debug = false) { 27 | 28 | $this->ip = $ip; 29 | $this->token = $token; 30 | $this->debug = $debug; 31 | 32 | if ($bind_ip != NULL) $this->bind_ip = $bind_ip; 33 | else $this->bind_ip = '0.0.0.0'; 34 | 35 | $this->dev = new miIO($this->ip, $this->bind_ip, $this->token, $this->debug); 36 | 37 | } 38 | 39 | /* 40 | Активирует авто-формирование уникальных ID сообщений с их сохранением в файл id.json 41 | */ 42 | 43 | public function enableAutoMsgID() { 44 | 45 | $this->dev->useAutoMsgID = true; 46 | 47 | } 48 | 49 | /* 50 | Деактивирует авто-формирование уникальных ID сообщений с их сохранением в файл id.json 51 | ID сообщений необходимо передавать в виде аргумента при каждой отправке команды, 52 | либо не указывать вообще, тогда ID будет 1 для всех сообщений. 53 | */ 54 | 55 | public function disableAutoMsgID() { 56 | 57 | $this->dev->useAutoMsgID = false; 58 | 59 | } 60 | 61 | /* 62 | Получить расширенные сведения 63 | */ 64 | 65 | public function getInfo($msg_id = 1) { 66 | 67 | if ($this->dev->getInfo($msg_id)) return $this->dev->data; 68 | else return false; 69 | 70 | } 71 | 72 | /* 73 | Получить текущий статус: 74 | power - питание (on или off), 75 | bright - яркость (от 1 до 100), 76 | cct - цветовая температура (от 1 до 100), 77 | snm - номер сцены (от 1 до 4), 78 | dv - таймер на выключение, макс. 6 часов (в секундах от 1 до 21600) 79 | */ 80 | 81 | public function getStatus($msg_id = 1) { 82 | 83 | $result = $this->dev->msgSendRcv('get_prop', '["power","bright","cct","snm","dv"]', $msg_id); 84 | 85 | if ($result) { 86 | if ($this->dev->data != '') { 87 | $res = json_decode($this->dev->data); 88 | if (isset($res->{'result'})) { 89 | $i = 0; 90 | foreach($this->status as $key => $value) { 91 | $this->status[$key] = $res->{'result'}[$i]; 92 | $i++; 93 | } 94 | return true; 95 | } else if (isset($res->{'error'})) { 96 | $this->error = $res->{'error'}->{'message'}; 97 | return false; 98 | } 99 | } else { 100 | $this->error = 'Нет данных'; 101 | return false; 102 | } 103 | } else { 104 | $this->error = 'Ответ не получен'; 105 | return false; 106 | } 107 | 108 | } 109 | 110 | /* 111 | Включить 112 | */ 113 | 114 | public function powerOn($msg_id = 1) { 115 | 116 | $result = $this->dev->msgSendRcv('set_power', '["on"]', $msg_id); 117 | return $this->verify($result); 118 | 119 | } 120 | 121 | /* 122 | Выключить 123 | */ 124 | 125 | public function powerOff($msg_id = 1) { 126 | 127 | $result = $this->dev->msgSendRcv('set_power', '["off"]', $msg_id); 128 | return $this->verify($result); 129 | 130 | } 131 | 132 | /* 133 | Установка яркости 134 | */ 135 | 136 | public function setBrightness($level = 50, $msg_id = 1) { 137 | 138 | if ( ($level < 1) or ($level > 100) ) $level = 50; 139 | $result = $this->dev->msgSendRcv('set_bright', "[$level]", $msg_id); 140 | return $this->verify($result); 141 | 142 | } 143 | 144 | /* 145 | Установка цветовой температуры 146 | */ 147 | 148 | public function setColorTemperature($level = 50, $msg_id = 1) { 149 | 150 | if ( ($level < 1) or ($level > 100) ) $level = 50; 151 | $result = $this->dev->msgSendRcv('set_cct', "[$level]", $msg_id); 152 | return $this->verify($result); 153 | 154 | } 155 | 156 | /* 157 | Переключение сцен - ярко, ТВ, тепло, полноч. 158 | */ 159 | 160 | public function setScene($num = 1, $msg_id = 1) { 161 | 162 | if ( ($num < 1) or ($num > 4) ) $num = 1; 163 | $result = $this->dev->msgSendRcv('apply_fixed_scene', "[$num]", $msg_id); 164 | return $this->verify($result); 165 | 166 | } 167 | 168 | /* 169 | Установка таймера на выключение 170 | */ 171 | 172 | public function setDelayOff($seconds = 60, $msg_id = 1) { 173 | 174 | if ( ($seconds < 0) or ($seconds > 21600) ) $seconds = 60; 175 | $result = $this->dev->msgSendRcv('delay_off', "[$seconds]", $msg_id); 176 | return $this->verify($result); 177 | 178 | } 179 | 180 | /* 181 | Проверка ответа 182 | */ 183 | 184 | private function verify ($result) { 185 | 186 | if ($result) { 187 | if ($this->dev->data != '') { 188 | $res = json_decode($this->dev->data); 189 | if (isset($res->{'result'})) { 190 | if ($res->{'result'}[0] == 'ok') return true; 191 | if ($res->{'result'}[0] == 'error') { 192 | $this->error = 'Unknown error.'; 193 | return false; 194 | } 195 | } else if (isset($res->{'error'})) { 196 | $this->error = $res->{'error'}->{'message'}; 197 | return false; 198 | } 199 | } else { 200 | $this->error = 'Нет данных'; 201 | return false; 202 | } 203 | } else { 204 | $this->error = 'Ответ не получен'; 205 | return false; 206 | } 207 | 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /devices/philipseyecare2.class.php: -------------------------------------------------------------------------------- 1 | '', 19 | 'bright' => '', 20 | 'notifystatus' => '', 21 | 'ambstatus' => '', 22 | 'ambvalue' => '', 23 | 'eyecare' => '', 24 | 'scene_num' => '', 25 | 'bls' => '', 26 | 'dvalue' => ''); 27 | 28 | public $dev = NULL; 29 | 30 | public function __construct($ip = NULL, $bind_ip = NULL, $token = NULL, $debug = false) { 31 | 32 | $this->ip = $ip; 33 | $this->token = $token; 34 | $this->debug = $debug; 35 | 36 | if ($bind_ip != NULL) $this->bind_ip = $bind_ip; 37 | else $this->bind_ip = '0.0.0.0'; 38 | 39 | $this->dev = new miIO($this->ip, $this->bind_ip, $this->token, $this->debug); 40 | 41 | } 42 | 43 | /* 44 | Активирует авто-формирование уникальных ID сообщений с их сохранением в файл id.json 45 | */ 46 | 47 | public function enableAutoMsgID() { 48 | 49 | $this->dev->useAutoMsgID = true; 50 | 51 | } 52 | 53 | /* 54 | Деактивирует авто-формирование уникальных ID сообщений с их сохранением в файл id.json 55 | ID сообщений необходимо передавать в виде аргумента при каждой отправке команды, 56 | либо не указывать вообще, тогда ID будет 1 для всех сообщений. 57 | */ 58 | 59 | public function disableAutoMsgID() { 60 | 61 | $this->dev->useAutoMsgID = false; 62 | 63 | } 64 | 65 | /* 66 | Получить расширенные сведения 67 | */ 68 | 69 | public function getInfo($msg_id = 1) { 70 | 71 | if ($this->dev->getInfo($msg_id)) return $this->dev->data; 72 | else return false; 73 | 74 | } 75 | 76 | /* 77 | Получить текущий статус: 78 | power - питание (on или off), 79 | bright - яркость (от 1 до 100), 80 | notifystatus - напоминание об усталости глаз, 81 | ambstatus - доп. боковая подсветка (on или off), 82 | ambvalue - яркость доп. боковой подсветки (от 1 до 100), 83 | eyecare - смарт режим безопасный для глаз (???), 84 | scene_num - номер сцены (от 1 до 3, study, reading, phone) 85 | bls - режим смарт-ночника (???), 86 | dvalue - таймер на выключение, макс. 60 минут (в минутах от 1 до 60). 87 | */ 88 | 89 | public function getStatus($msg_id = 1) { 90 | 91 | $result = $this->dev->msgSendRcv('get_prop', '["power","bright","notifystatus","ambstatus","ambvalue","eyecare","scene_num","bls","dvalue"]', $msg_id); 92 | 93 | if ($result) { 94 | if ($this->dev->data != '') { 95 | $res = json_decode($this->dev->data); 96 | if (isset($res->{'result'})) { 97 | $i = 0; 98 | foreach($this->status as $key => $value) { 99 | $this->status[$key] = $res->{'result'}[$i]; 100 | $i++; 101 | } 102 | return true; 103 | } else if (isset($res->{'error'})) { 104 | $this->error = $res->{'error'}->{'message'}; 105 | return false; 106 | } 107 | } else { 108 | $this->error = 'Нет данных'; 109 | return false; 110 | } 111 | } else { 112 | $this->error = 'Ответ не получен'; 113 | return false; 114 | } 115 | 116 | } 117 | 118 | /* 119 | Включить 120 | */ 121 | 122 | public function powerOn($msg_id = 1) { 123 | 124 | $result = $this->dev->msgSendRcv('set_power', '["on"]', $msg_id); 125 | return $this->verify($result); 126 | 127 | } 128 | 129 | /* 130 | Выключить 131 | */ 132 | 133 | public function powerOff($msg_id = 1) { 134 | 135 | $result = $this->dev->msgSendRcv('set_power', '["off"]', $msg_id); 136 | return $this->verify($result); 137 | 138 | } 139 | 140 | /* 141 | Установка яркости 142 | */ 143 | 144 | public function setBrightness($level = 50, $msg_id = 1) { 145 | 146 | if ( ($level < 1) or ($level > 100) ) $level = 50; 147 | $result = $this->dev->msgSendRcv('set_bright', "[$level]", $msg_id); 148 | return $this->verify($result); 149 | 150 | } 151 | 152 | /* 153 | Проверка ответа 154 | */ 155 | 156 | private function verify ($result) { 157 | 158 | if ($result) { 159 | if ($this->dev->data != '') { 160 | $res = json_decode($this->dev->data); 161 | if (isset($res->{'result'})) { 162 | if ($res->{'result'}[0] == 'ok') return true; 163 | if ($res->{'result'}[0] == 'error') { 164 | $this->error = 'Unknown error.'; 165 | return false; 166 | } 167 | } else if (isset($res->{'error'})) { 168 | $this->error = $res->{'error'}->{'message'}; 169 | return false; 170 | } 171 | } else { 172 | $this->error = 'Нет данных'; 173 | return false; 174 | } 175 | } else { 176 | $this->error = 'Ответ не получен'; 177 | return false; 178 | } 179 | 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /img/discoverall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/discoverall.png -------------------------------------------------------------------------------- /img/miIO_hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/miIO_hello.png -------------------------------------------------------------------------------- /img/miIO_пакет.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/miIO_пакет.png -------------------------------------------------------------------------------- /img/miio2db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/miio2db.png -------------------------------------------------------------------------------- /img/mitoolkit_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/mitoolkit_1.png -------------------------------------------------------------------------------- /img/mitoolkit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/mitoolkit_2.png -------------------------------------------------------------------------------- /img/mitoolkit_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/mitoolkit_3.png -------------------------------------------------------------------------------- /img/mitoolkit_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/mitoolkit_4.png -------------------------------------------------------------------------------- /img/mitoolkit_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/mitoolkit_5.png -------------------------------------------------------------------------------- /img/mitoolkit_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/mitoolkit_6.png -------------------------------------------------------------------------------- /img/packethandler1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/packethandler1.png -------------------------------------------------------------------------------- /img/packethandler2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/packethandler2.png -------------------------------------------------------------------------------- /img/packsender1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/packsender1.png -------------------------------------------------------------------------------- /img/sqlite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/sqlite.png -------------------------------------------------------------------------------- /img/wifi_ap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skysilver-lab/php-miio/67145d902bc981a62c1bf6cc89b8b9770e3bb76e/img/wifi_ap.png -------------------------------------------------------------------------------- /miio-cli.php: -------------------------------------------------------------------------------- 1 | fastDiscover(); 92 | 93 | echo PHP_EOL . 'Используем авто-формирование уникальных ID для команд из файла id.json' . PHP_EOL; 94 | $dev->useAutoMsgID = true; 95 | 96 | if ($dev->getInfo() == true) { 97 | echo 'Информация об устройстве:' . PHP_EOL; 98 | echo $dev->data . PHP_EOL; 99 | } else { 100 | echo 'Устройство не отвечает.' . PHP_EOL; 101 | } 102 | } 103 | 104 | if ( isset($opts['sendcmd']) && empty($opts['ip']) ) { 105 | echo 'Необходимо указать ip-адрес устройства через параметр --ip' . PHP_EOL; 106 | echo ' php miio-cli.php --ip 192.168.1.45 --info' . PHP_EOL; 107 | } else if (isset($opts['sendcmd']) && empty($opts['sendcmd']) && !empty($opts['ip'])) { 108 | echo 'Необходимо указать команду' . PHP_EOL; 109 | echo ' php miio-cli.php --ip 192.168.1.45 --sendcmd \'{\'method\': \'get_status\', \'id\': 1}\'' . PHP_EOL; 110 | } else if (!empty($opts['sendcmd']) && !empty($opts['ip'])) { 111 | if (isset($opts['token']) && !empty($opts['token'])) $dev = new miIO($opts['ip'], $bind_ip, $opts['token'], $debug); 112 | else $dev = new miIO($opts['ip'], $bind_ip, null, $debug); 113 | if ($dev->msgSendRcvRaw($opts['sendcmd']) == true) { 114 | echo "Устройство $dev->ip доступно и ответило:" . PHP_EOL; 115 | echo $dev->data . PHP_EOL; 116 | } else { 117 | echo "Устройство $dev->ip не доступно или не отвечает." . PHP_EOL; 118 | } 119 | } 120 | 121 | if ( isset($opts['decode']) && empty($opts['token']) ) { 122 | echo 'Необходимо указать токен устройства через параметр --token' . PHP_EOL; 123 | } else if (isset($opts['decode']) && !empty($opts['decode']) && isset($opts['token']) && !empty($opts['token'])) { 124 | $miPacket = new miPacket(); 125 | $miPacket->setToken($opts['token']); 126 | $miPacket->msgParse($opts['decode']); 127 | if ($debug) $miPacket->printPacket(); 128 | $data_dec = $miPacket->decryptData($miPacket->data); 129 | echo "Расшифрованные данные: $data_dec" . PHP_EOL; 130 | } 131 | 132 | function cbDiscoverAll ($bind_ip, $debug) { 133 | 134 | $dev = new miIO(null, $bind_ip, null, $debug); 135 | 136 | //echo PHP_EOL . 'Отправка предварительного широковещательного hello-пакета' . PHP_EOL; 137 | //$dev->fastDiscover(); 138 | 139 | if ($dev->discover() == true) { 140 | echo 'Поиск выполнен.' . PHP_EOL; 141 | $devices = json_decode($dev->data); 142 | $count = count($devices->devices); 143 | echo "Найдено $count устройств." . PHP_EOL; 144 | foreach($devices->devices as $dev) { 145 | $devprop = json_decode($dev); 146 | echo ' IP ' . $devprop->ip . 147 | ' DevType ' . $devprop->devicetype . 148 | ' Serial ' . $devprop->serial . 149 | ' Token ' . $devprop->token . PHP_EOL; 150 | } 151 | } else { 152 | echo 'Поиск выполнен. Устройств не найдено.' . PHP_EOL; 153 | } 154 | } 155 | 156 | function cbDiscoverIP ($ip, $bind_ip, $debug) { 157 | 158 | $dev = new miIO($ip, $bind_ip, null, $debug); 159 | 160 | if ($dev->discover($ip) == true) { 161 | echo 'Поиск выполнен.' . PHP_EOL; 162 | echo 'Устройство найдено и отвечает.' . PHP_EOL; 163 | } else { 164 | echo 'Поиск выполнен. Устройств не найдено.' . PHP_EOL; 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /miio-sample.php: -------------------------------------------------------------------------------- 1 | ' . PHP_EOL . PHP_EOL; 21 | 22 | $dev = new miIO(null, $bind_ip, null, $debug); 23 | 24 | if ($dev->discover() == true) { 25 | echo 'Поиск выполнен.' . PHP_EOL; 26 | $devices = json_decode($dev->data); 27 | $count = count($devices->devices); 28 | echo "Найдено $count устройств." . PHP_EOL; 29 | foreach($devices->devices as $dev) { 30 | $devprop = json_decode($dev); 31 | echo ' IP ' . $devprop->ip . 32 | ' DevType ' . $devprop->devicetype . 33 | ' Serial ' . $devprop->serial . 34 | ' Token ' . $devprop->token . PHP_EOL; 35 | $d = new miIO($devprop->ip, $devprop->token, $debug); 36 | $d->getInfo(); 37 | echo $d->data . PHP_EOL; 38 | } 39 | } else { 40 | echo 'Поиск выполнен. Устройство не найдено.' . PHP_EOL; 41 | } 42 | 43 | /* 44 | 45 | $dev = new miIO($ip, $bind_ip, null, $debug); 46 | 47 | echo PHP_EOL . "Отправляем команду на $dev->ip" . PHP_EOL; 48 | 49 | if ($dev->msgSendRcv('miIO.info', '[]') == true) { 50 | echo "Устройство $dev->ip доступно и ответило:" . PHP_EOL; 51 | echo $dev->data . PHP_EOL; 52 | } else { 53 | echo "Устройство $dev->ip не доступно или не отвечает." . PHP_EOL; 54 | } 55 | 56 | sleep(2); 57 | 58 | if ($dev->msgSendRcvRaw('{"id":1,"method":"toggle","params":[]}') == true) { 59 | echo "Устройство $dev->ip доступно и ответило:" . PHP_EOL; 60 | echo $dev->data . PHP_EOL; 61 | } else { 62 | echo "Устройство $dev->ip не доступно или не отвечает." . PHP_EOL; 63 | } 64 | 65 | sleep(2); 66 | 67 | if ($dev->getInfo() == true) { 68 | echo "Устройство $dev->ip доступно и ответило:" . PHP_EOL; 69 | echo $dev->data . PHP_EOL; 70 | } else { 71 | echo "Устройство $dev->ip не доступно или не отвечает." . PHP_EOL; 72 | } 73 | */ 74 | 75 | echo PHP_EOL . '<----- php-miio end ----->' . PHP_EOL; 76 | echo PHP_EOL; 77 | -------------------------------------------------------------------------------- /miio.class.php: -------------------------------------------------------------------------------- 1 | debug = $debug; 45 | 46 | $this->miPacket = new miPacket(); 47 | 48 | if ($ip != NULL) $this->ip = $ip; 49 | 50 | if ($bind_ip != NULL) $this->bind_ip = $bind_ip; 51 | else $this->bind_ip = '0.0.0.0'; 52 | 53 | if ($token != NULL) $this->token = $token; 54 | 55 | if ($this->debug) { 56 | if ($this->ip == NULL) echo "Режим широковещательного поиска устройств" . PHP_EOL; 57 | else echo "Соединение с устройством IP $this->ip" . PHP_EOL; 58 | echo "Статус отладки [$this->debug]" . PHP_EOL; 59 | } 60 | 61 | $this->sockCreate(); 62 | 63 | } 64 | 65 | public function __destruct() { 66 | 67 | @socket_shutdown($this->sock, 2); 68 | @socket_close($this->sock); 69 | 70 | } 71 | 72 | /* 73 | Создание udp4 сокета. 74 | */ 75 | 76 | public function sockCreate() { 77 | 78 | if (!($this->sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP))) { 79 | $errorcode = socket_last_error(); 80 | $errormsg = socket_strerror($errorcode); 81 | if ($this->debug) echo "Ошибка создания сокета - [socket_create()] [$errorcode] $errormsg" . PHP_EOL; 82 | die("Ошибка создания сокета - [socket_create()] [$errorcode] $errormsg \n"); 83 | } else { if ($this->debug) echo 'Сокет успешно создан' . PHP_EOL; } 84 | 85 | } 86 | 87 | /* 88 | Установка параметров сокета - таймаут. 89 | */ 90 | 91 | public function sockSetTimeout($timeout = 2) { 92 | 93 | if (!socket_set_option($this->sock, SOL_SOCKET, SO_RCVTIMEO, array("sec" => $timeout, "usec" => 0))) { 94 | $errorcode = socket_last_error(); 95 | $errormsg = socket_strerror($errorcode); 96 | if ($this->debug) echo "Ошибка установки параметра SO_RCVTIMEO сокета - [socket_create()] [$errorcode] $errormsg" . PHP_EOL; 97 | } else { if ($this->debug) echo 'Параметр SO_RCVTIMEO сокета успешно задан' . PHP_EOL; } 98 | 99 | } 100 | 101 | /* 102 | Установка параметров сокета - броадкаст. 103 | */ 104 | 105 | public function sockSetBroadcast() { 106 | 107 | if (!socket_set_option($this->sock, SOL_SOCKET, SO_BROADCAST, 1)) { 108 | $errorcode = socket_last_error(); 109 | $errormsg = socket_strerror($errorcode); 110 | if ($this->debug) echo "Ошибка установки параметра SO_BROADCAST сокета - [socket_create()] [$errorcode] $errormsg" . PHP_EOL; 111 | } else { if ($this->debug) echo 'Параметр SO_BROADCAST сокета успешно задан' . PHP_EOL; } 112 | 113 | } 114 | 115 | /* 116 | Поиск устройства и начало сессии с ним. 117 | */ 118 | 119 | public function discover($ip = NULL) { 120 | 121 | if ($ip != NULL) { 122 | 123 | if ($this->debug) echo PHP_EOL . "Проверяем доступность устройства $ip" . PHP_EOL; 124 | 125 | $this->sockSetTimeout($this->send_timeout); 126 | 127 | if ($this->debug) echo " >>>>> Отправляем hello-пакет на $ip с таймаутом $this->send_timeout" . PHP_EOL; 128 | 129 | $helloPacket = hex2bin(HELLO_MSG); 130 | 131 | if(!($bytes = socket_sendto($this->sock, $helloPacket, strlen($helloPacket), 0, $ip, MIIO_PORT))) { 132 | $errorcode = socket_last_error(); 133 | $errormsg = socket_strerror($errorcode); 134 | if ($this->debug) echo "Не удалось отправить данные в сокет [$errorcode] $errormsg" . PHP_EOL; 135 | } else { if ($this->debug) echo " >>>>> Отправлено в сокет $bytes байт" . PHP_EOL; } 136 | 137 | $buf = ''; 138 | if (($bytes = @socket_recvfrom($this->sock, $buf, 4096, 0, $remote_ip, $remote_port)) !== false) { 139 | if ($buf != '') { 140 | if ($this->debug) { 141 | echo " <<<<< Получен ответ от IP $remote_ip с порта $remote_port" . PHP_EOL; 142 | if ($this->debug) echo "Прочитано $bytes байта из сокета" . PHP_EOL; 143 | } 144 | $this->miPacket->msgParse(bin2hex($buf)); 145 | if ($this->debug) { 146 | $this->miPacket->printHead(); 147 | $timediff = hexdec($this->miPacket->ts) - time(); 148 | $ts_server = time(); 149 | echo 'ts_server: ' . dechex($ts_server) . ' --> ' . $ts_server . ' секунд' . ' --> ' . date('Y-m-d H:i:s', $ts_server) . PHP_EOL; 150 | echo 'timediff: ' . $timediff . PHP_EOL; 151 | } 152 | return true; 153 | } 154 | } else if ($bytes === 0 || $bytes === false) { 155 | $errorcode = socket_last_error(); 156 | $errormsg = socket_strerror($errorcode); 157 | if ($this->debug) echo "Ошибка чтения из сокета [$errorcode] $errormsg" . PHP_EOL; 158 | return false; 159 | } 160 | } else { 161 | 162 | if ($this->debug) echo PHP_EOL . 'Поиск доступных устройств в локальной сети (handshake discovery)' . PHP_EOL; 163 | 164 | $this->sockSetTimeout($this->disc_timeout); 165 | 166 | $this->sockSetBroadcast(); 167 | 168 | if( !@socket_bind($this->sock, $this->bind_ip , 0) ) { 169 | $errorcode = socket_last_error(); 170 | $errormsg = socket_strerror($errorcode); 171 | if ($this->debug) echo "Не удалось привязать сокет к адресу $this->bind_ip [$errorcode] $errormsg" . PHP_EOL; 172 | } else { if ($this->debug) echo "Сокет успешно привязан к адресу $this->bind_ip" . PHP_EOL; } 173 | 174 | $ip = '255.255.255.255'; 175 | 176 | if ($this->debug) echo " >>>>> Отправляем hello-пакет на $ip с таймаутом $this->disc_timeout" . PHP_EOL; 177 | 178 | $helloPacket = hex2bin(HELLO_MSG); 179 | 180 | if(!($bytes = socket_sendto($this->sock, $helloPacket, strlen($helloPacket), 0, $ip, MIIO_PORT))) { 181 | $errorcode = socket_last_error(); 182 | $errormsg = socket_strerror($errorcode); 183 | if ($this->debug) echo "Не удалось отправить данные в сокет [$errorcode] $errormsg" . PHP_EOL; 184 | } else { if ($this->debug) echo " >>>>> Отправлено в сокет $bytes байт" . PHP_EOL; } 185 | 186 | $buf = ''; 187 | $count = 0; 188 | $devinfo = array(); 189 | $devices = array(); 190 | 191 | while ($bytes = @socket_recvfrom($this->sock, $buf, 4096, 0, $remote_ip, $remote_port)) { 192 | if ($buf != '') { 193 | if ($this->debug) { 194 | echo ($count+1) . " <<<<< Получен ответ от IP $remote_ip с порта $remote_port" . PHP_EOL; 195 | if ($this->debug) echo "Прочитано $bytes байта из сокета" . PHP_EOL; 196 | } 197 | $this->miPacket->msgParse(bin2hex($buf)); 198 | 199 | if ($this->debug) { 200 | $this->miPacket->printHead(); 201 | $timediff = hexdec($this->miPacket->ts) - time(); 202 | $ts_server = time(); 203 | echo 'ts_server: ' . dechex($ts_server) . ' --> ' . $ts_server . ' секунд' . ' --> ' . date('Y-m-d H:i:s', $ts_server) . PHP_EOL; 204 | echo 'timediff: ' . $timediff . PHP_EOL; 205 | } 206 | 207 | $devinfo = $this->miPacket->info; 208 | $devinfo += ["ip" => $remote_ip]; 209 | $devices[] = json_encode($devinfo); 210 | } 211 | $count += 1; 212 | if ($bytes === 0 || $bytes === false) { 213 | $errorcode = socket_last_error(); 214 | $errormsg = socket_strerror($errorcode); 215 | if ($this->debug) echo "Ошибка чтения из сокета [$errorcode] $errormsg" . PHP_EOL; 216 | } 217 | } 218 | 219 | if(!empty($devices)) $this->data = '{"devices":'. json_encode($devices) .'}'; 220 | 221 | if ($count != 0 || !empty($this->data)) return true; 222 | else return false; 223 | } 224 | } 225 | 226 | public function fastDiscover() { 227 | 228 | $timeout = 2; 229 | 230 | $this->sockSetTimeout($timeout); 231 | $this->sockSetBroadcast(); 232 | 233 | if( !@socket_bind($this->sock, $this->bind_ip , 0) ) { 234 | $errorcode = socket_last_error(); 235 | $errormsg = socket_strerror($errorcode); 236 | if ($this->debug) echo " --> Не удалось привязать сокет к адресу $this->bind_ip [$errorcode] $errormsg" . PHP_EOL; 237 | } else { if ($this->debug) echo " --> Сокет успешно привязан к адресу $this->bind_ip" . PHP_EOL; } 238 | 239 | $ip = '255.255.255.255'; 240 | 241 | if ($this->debug) echo " --> Отправляем hello-пакет на $ip с таймаутом $timeout" . PHP_EOL; 242 | 243 | $helloPacket = hex2bin(HELLO_MSG); 244 | 245 | if(!($bytes = socket_sendto($this->sock, $helloPacket, strlen($helloPacket), 0, $ip, MIIO_PORT))) { 246 | $errorcode = socket_last_error(); 247 | $errormsg = socket_strerror($errorcode); 248 | if ($this->debug) echo " --> Не удалось отправить данные в сокет [$errorcode] $errormsg" . PHP_EOL . PHP_EOL; 249 | } else { if ($this->debug) echo " --> Отправлено в сокет $bytes байт" . PHP_EOL . PHP_EOL; } 250 | 251 | } 252 | 253 | /* 254 | Сокеты. Запись и чтение. 255 | */ 256 | 257 | public function socketWriteRead($msg) { 258 | 259 | if ($this->discover($this->ip)) { 260 | 261 | if ($this->debug) echo PHP_EOL . "Устройство $this->ip доступно" . PHP_EOL; 262 | 263 | $this->sockSetTimeout($this->send_timeout); 264 | 265 | if ($this->token != NULL) { 266 | if(!$this->miPacket->setToken($this->token)) { 267 | if ($this->debug) echo 'Неверный формат токена!' . PHP_EOL; 268 | die('Неверный формат токена!\n'); 269 | } else { 270 | if ($this->debug) echo 'Используется токен, указанный вручную, - ' . $this->token . PHP_EOL; 271 | } 272 | } else { 273 | if ($this->debug) echo 'Используется токен, полученный от устройства, - ' . $this->miPacket->getToken() . PHP_EOL; 274 | } 275 | 276 | if ($this->debug) echo " >>>>> Отправляем пакет на $this->ip с таймаутом $this->send_timeout" . PHP_EOL; 277 | 278 | $packet = hex2bin($this->miPacket->msgBuild($msg)); 279 | 280 | if ($this->debug) { 281 | $this->miPacket->printHead(); 282 | $timediff = hexdec($this->miPacket->ts) - time(); 283 | echo 'data: ' . $this->miPacket->data . PHP_EOL; 284 | $ts_server = time(); 285 | echo 'ts_server: ' . dechex($ts_server) . ' --> ' . $ts_server . ' секунд' . ' --> ' . date('Y-m-d H:i:s', $ts_server) . PHP_EOL; 286 | echo 'timediff: ' . $timediff . PHP_EOL; 287 | } 288 | 289 | if(!($bytes = socket_sendto($this->sock, $packet, strlen($packet), 0, $this->ip, MIIO_PORT))) { 290 | $errorcode = socket_last_error(); 291 | $errormsg = socket_strerror($errorcode); 292 | if ($this->debug) echo "Не удалось отправить данные в сокет [$errorcode] $errormsg" . PHP_EOL; 293 | } else { if ($this->debug) echo " >>>>> Отправлено в сокет $bytes байт" . PHP_EOL; } 294 | 295 | $this->miPacket->data = ''; 296 | 297 | $buf = ''; 298 | if (($bytes = @socket_recvfrom($this->sock, $buf, 4096, 0, $remote_ip, $remote_port)) !== false) { 299 | if ($buf != '') { 300 | if ($this->debug) { 301 | echo " <<<<< Получен ответ от IP $remote_ip с порта $remote_port" . PHP_EOL; 302 | if ($this->debug) echo "Прочитано $bytes байта из сокета" . PHP_EOL; 303 | } 304 | $this->miPacket->msgParse(bin2hex($buf)); 305 | if ($this->debug) $this->miPacket->printPacket(); 306 | $data_dec = $this->miPacket->decryptData($this->miPacket->data); 307 | if ($this->debug) echo "Расшифрованные данные: $data_dec" . PHP_EOL; 308 | //проверить json на валидность 309 | json_decode($data_dec); 310 | if ($jsonErrCode = json_last_error() !== JSON_ERROR_NONE) { 311 | $jsonErrMsg = $this->jsonLastErrorMsg(); 312 | if ($this->debug) echo "Данные JSON не валидны. Ошибка: $jsonErrMsg" . PHP_EOL; 313 | if ($jsonErrCode == JSON_ERROR_CTRL_CHAR) { 314 | // если ошибка в управляющих символах, то удаляем хвосты в начале и в конце и возвращаем 315 | if ($this->debug) echo 'Выполняем trim()' . PHP_EOL; 316 | $this->data = trim($data_dec); 317 | return true; 318 | } else { 319 | // если иная ошибка, возвращаем как есть для обработки на верхнем уровне 320 | $this->data = $data_dec; 321 | } 322 | } else { 323 | // если ошибок нет, то возвращаем как есть 324 | if ($this->debug) echo 'Данные JSON валидны.' . PHP_EOL; 325 | $this->data = $data_dec; 326 | } 327 | return true; 328 | } 329 | } else if ($bytes === 0 || $bytes === false) { 330 | $errorcode = socket_last_error(); 331 | $errormsg = socket_strerror($errorcode); 332 | if ($this->debug) echo "Ошибка чтения из сокета [$errorcode] $errormsg" . PHP_EOL; 333 | return false; 334 | } 335 | } else { 336 | if ($this->debug) echo "Устройство по адресу $this->ip не ответило на hello-запрос!" . PHP_EOL; 337 | return false; 338 | } 339 | } 340 | 341 | /* 342 | Отправка сообщения (метод и параметры раздельно) устройству и прием ответа. 343 | */ 344 | 345 | public function msgSendRcv($command, $parameters = NULL, $id = 1) { 346 | 347 | if (isset($id) && ($id > 0) && !$this->useAutoMsgID) $this->msg_id = $id; 348 | else if ($this->useAutoMsgID) $this->msg_id = $this->getMsgID($this->ip); 349 | 350 | $msg = '{"id":' . $this->msg_id . ',"method":"'. $command . '"}'; 351 | 352 | if ($parameters != NULL) { 353 | $msg = '{"id":' . $this->msg_id . ',"method":"'. $command . '","params":' . $parameters . '}'; 354 | } 355 | 356 | if ($this->debug) echo "Команда для отправки - $msg" . PHP_EOL; 357 | 358 | return $this->socketWriteRead($msg); 359 | 360 | } 361 | 362 | /* 363 | Отправка сообщения (как есть) устройству и прием ответа. 364 | */ 365 | 366 | public function msgSendRcvRaw($msg) { 367 | 368 | if (substr_count($msg, "'") > 0 ) $msg = str_replace("'", '"', $msg); 369 | 370 | if ($this->debug) echo "Команда для отправки - $msg" . PHP_EOL; 371 | 372 | return $this->socketWriteRead($msg); 373 | 374 | } 375 | 376 | /* 377 | Получить новый идентификатор для команды. 378 | */ 379 | 380 | public function getMsgID($ip) { 381 | 382 | if (file_exists ('id.json')) { 383 | $file = file_get_contents('id.json'); 384 | $ids = json_decode($file, TRUE); 385 | } else { 386 | file_put_contents('id.json', ''); 387 | $ids = array(); 388 | } 389 | 390 | if (!empty($ids)) { 391 | if (array_key_exists($ip, $ids)) { 392 | if ($ids[$ip] > 1000) $ids[$ip] = 1; 393 | else $ids[$ip] += 1; 394 | } else { 395 | $ids += [$ip => 1]; 396 | } 397 | } else { 398 | $ids = [$ip => 1]; 399 | } 400 | 401 | file_put_contents('id.json', json_encode($ids)); 402 | 403 | return $ids[$ip]; 404 | } 405 | 406 | /* 407 | Получить описание ошибки JSON. 408 | (определяем функцию, если старая версия PHP) 409 | */ 410 | 411 | public function jsonLastErrorMsg() { 412 | 413 | if (!function_exists('json_last_error_msg')) { 414 | 415 | function json_last_error_msg() { 416 | 417 | static $ERRORS = array(JSON_ERROR_NONE => 'No error has occurred', 418 | JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded', 419 | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', 420 | JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', 421 | JSON_ERROR_SYNTAX => 'Syntax error', 422 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'); 423 | 424 | $error = json_last_error(); 425 | return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error'; 426 | } 427 | } 428 | 429 | return json_last_error_msg(); 430 | 431 | } 432 | 433 | /* 434 | Получить miIO-сведения об устройстве. 435 | */ 436 | 437 | public function getInfo($msg_id = 1) { 438 | 439 | return $this->msgSendRcv('miIO.info', '[]', $msg_id); 440 | 441 | } 442 | 443 | } 444 | -------------------------------------------------------------------------------- /mipacket.class.php: -------------------------------------------------------------------------------- 1 | '', 29 | 'serial' => '', 30 | 'token' => ''); 31 | 32 | private $token = ''; 33 | private $key = ''; 34 | private $iv = ''; 35 | private $firstSet = true; 36 | 37 | /* 38 | Сохранение токена. 39 | */ 40 | 41 | public function setToken($token) { 42 | 43 | if ($this->verifyToken($token)) { 44 | $this->token = $token; 45 | $this->getKeyIv(); 46 | return true; 47 | } else { 48 | return false; 49 | }; 50 | 51 | } 52 | 53 | /* 54 | Вывод токена. 55 | */ 56 | 57 | public function getToken() { 58 | 59 | return $this->token; 60 | 61 | } 62 | 63 | /* 64 | Проверка длины токена. 65 | */ 66 | 67 | private function verifyToken($token) { 68 | 69 | if (strlen($token) == 32) return true; 70 | else return false; 71 | 72 | } 73 | 74 | /* 75 | Вычисление ключа шифрования и вектора первичной инициализации на основе токена. 76 | */ 77 | 78 | private function getKeyIv() { 79 | 80 | $this->key = md5(hex2bin($this->token)); 81 | $this->iv = md5(hex2bin($this->key.$this->token)); 82 | 83 | } 84 | 85 | /* 86 | Шифрование данных. 87 | */ 88 | 89 | public function encryptData($data) { 90 | 91 | if ($this->verifyToken($this->token)) { 92 | return bin2hex(openssl_encrypt($data, 'AES-128-CBC', hex2bin($this->key), OPENSSL_RAW_DATA, hex2bin($this->iv))); 93 | } else return false; 94 | 95 | } 96 | 97 | /* 98 | Расшифровка данных. 99 | */ 100 | 101 | public function decryptData($data) { 102 | 103 | if ($this->verifyToken($this->token)) { 104 | return openssl_decrypt(hex2bin($data), 'AES-128-CBC', hex2bin($this->key), OPENSSL_RAW_DATA, hex2bin($this->iv)); 105 | } else return false; 106 | 107 | } 108 | 109 | /* 110 | Формирование пакета. 111 | */ 112 | 113 | public function msgBuild($cmd) { 114 | 115 | $this->data = $this->encryptData($cmd); 116 | 117 | $this->length = sprintf('%04x', (int)strlen($this->data)/2 + 32); 118 | 119 | //$this->ts = sprintf('%08x', hexdec($this->ts)); // продублировать локальное время устройства 120 | //$this->ts = sprintf('%08x', (hexdec($this->ts) + 1)); // плюс 1 секунда к локальному времени устройства 121 | //$this->ts = sprintf('%08x', time()); // текущее время сервера 122 | $this->ts = sprintf('%08x', time() + $this->timediff); // с учетом разницы времени между устройством и сервером 123 | 124 | $packet = $this->magic.$this->length.$this->unknown1.$this->devicetype.$this->serial.$this->ts.$this->token.$this->data; 125 | 126 | $this->checksum = md5(hex2bin($packet)); 127 | 128 | $packet = $this->magic.$this->length.$this->unknown1.$this->devicetype.$this->serial.$this->ts.$this->checksum.$this->data; 129 | 130 | return $packet; 131 | 132 | } 133 | 134 | /* 135 | Разбор пакета по полям. 136 | */ 137 | 138 | public function msgParse($msg) { 139 | 140 | $this->magic = substr($msg, 0, 4); 141 | $this->length = substr($msg, 4, 4); 142 | $this->unknown1 = substr($msg, 8, 8); 143 | $this->devicetype = substr($msg, 16, 4); 144 | $this->serial = substr($msg, 20, 4); 145 | $this->ts = substr($msg, 24, 8); 146 | $this->checksum = substr($msg, 32, 32); 147 | 148 | if ( ($this->length == '0020') && (strlen($msg)/2 == 32) ) { 149 | $this->setToken(substr($msg, 32, 32)); 150 | // запомним разницу времени между устройством и сервером 151 | $timediff = hexdec($this->ts) - time(); 152 | if ($this->firstSet && $timediff != 0) { 153 | $this->timediff = $timediff; 154 | } 155 | if ($this->firstSet) $this->firstSet = false; 156 | } else { 157 | $data_length = strlen($msg) - 64; 158 | if ($data_length > 0) { 159 | $this->data = substr($msg, 64, $data_length); 160 | } 161 | } 162 | 163 | $this->info['devicetype'] = $this->devicetype; 164 | $this->info['serial'] = $this->serial; 165 | $this->info['token'] = $this->token; 166 | 167 | } 168 | 169 | /* 170 | Вывод заголовка пакета. 171 | */ 172 | 173 | public function printHead() { 174 | 175 | echo 'magic: ' . $this->magic . PHP_EOL; 176 | echo 'length: ' . $this->length . ' --> ' . hexdec($this->length) . ' байт' . PHP_EOL; 177 | echo 'unknown1: ' . $this->unknown1 . PHP_EOL; 178 | echo 'devicetype: ' . $this->devicetype . PHP_EOL; 179 | echo 'serial: ' . $this->serial . PHP_EOL; 180 | echo 'ts: ' . $this->ts . ' --> ' . hexdec($this->ts) . ' секунд' . ' --> ' . date('Y-m-d H:i:s', hexdec($this->ts)) . PHP_EOL; 181 | echo 'checksum: ' . $this->checksum . PHP_EOL; 182 | 183 | } 184 | 185 | /* 186 | Вывод полей пакета и данных. 187 | */ 188 | 189 | public function printPacket() { 190 | 191 | $this->printHead(); 192 | echo 'data: ' . $this->data . PHP_EOL; 193 | 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /mirobot-sample.php: -------------------------------------------------------------------------------- 1 | enableAutoMsgID(); 21 | 22 | echo PHP_EOL . date('H:i:s', time()); 23 | echo PHP_EOL; 24 | 25 | if ($mirobot->getStatus()) { 26 | 27 | echo 'Код состояния: ' . $mirobot->status['state'] . PHP_EOL; 28 | echo 'Состояние: ' . $mirobot->status['state_text'] . PHP_EOL; 29 | echo 'Заряд батареи, %: ' . $mirobot->status['battery'] . PHP_EOL; 30 | echo 'Мощность турбины, %: ' . $mirobot->status['fan_power'] . PHP_EOL; 31 | echo 'Код ошибки: ' . $mirobot->status['error_code'] . PHP_EOL; 32 | echo 'Описание ошибки: ' . $mirobot->status['error_text'] . PHP_EOL; 33 | echo 'Площадь уборки, кв. см.: ' . $mirobot->status['clean_area'] . PHP_EOL; 34 | echo 'Время уборки, сек.: ' . $mirobot->status['clean_time'] . PHP_EOL; 35 | echo 'Режим "не беспокоить": ' . $mirobot->status['dnd_enabled'] . PHP_EOL; 36 | echo 'В процессе уборки или нет: ' . $mirobot->status['in_cleaning'] . PHP_EOL; 37 | echo 'Есть карта или нет: ' . $mirobot->status['map_present'] . PHP_EOL; 38 | echo 'Версия команд: ' . $mirobot->status['msg_ver'] . PHP_EOL; 39 | echo 'Счетчик команд: ' . $mirobot->status['msg_seq'] . PHP_EOL; 40 | 41 | } else echo " Статус не получен. Ошибка: $mirobot->error" . PHP_EOL; 42 | 43 | /* 44 | echo $mirobot->getInfo() . PHP_EOL; 45 | 46 | if ($mirobot->start()) echo ' Начата уборка.' . PHP_EOL; 47 | else echo " Не удалось начать уборку. Ошибка: $mirobot->error" . PHP_EOL; 48 | 49 | if ($mirobot->stop()) echo ' Уборка остановлена.' . PHP_EOL; 50 | else echo " Не удалось остановить уборку. Ошибка: $mirobot->error" . PHP_EOL; 51 | 52 | if ($mirobot->pause()) echo ' Уборка приостановлена.' . PHP_EOL; 53 | else echo " Не удалось приостановить уборку. Ошибка: $mirobot->error" . PHP_EOL; 54 | 55 | if ($mirobot->cleanSpot()) echo ' Начата уборка cleanSpot.' . PHP_EOL; 56 | else echo " Не удалось начать уборку cleanSpot. Ошибка: $mirobot->error" . PHP_EOL; 57 | 58 | if ($mirobot->charge()) echo ' Возвращение на базу.' . PHP_EOL; 59 | else echo " Не удалось вернуться на базу. Ошибка: $mirobot->error" . PHP_EOL; 60 | 61 | if ($mirobot->findMe()) echo ' Отправлена команда поиска.' . PHP_EOL; 62 | else echo " Не удалось отправить команду поиска. Ошибка: $mirobot->error" . PHP_EOL; 63 | 64 | */ 65 | -------------------------------------------------------------------------------- /philipsbulb-sample.php: -------------------------------------------------------------------------------- 1 | enableAutoMsgID(); 26 | 27 | echo PHP_EOL . date('H:i:s', time()); 28 | 29 | if($bulb->getStatus($cmd_id)) { 30 | echo ' Статус получен.' . PHP_EOL; 31 | echo 'Питание: ' . $bulb->status['power'] . PHP_EOL; 32 | echo 'Яркость: ' . $bulb->status['bright'] . PHP_EOL; 33 | echo 'Цветовая температура: ' . $bulb->status['cct'] . PHP_EOL; 34 | echo 'Сцена: ' . $bulb->status['snm'] . PHP_EOL; 35 | echo 'Таймер выключения: ' . $bulb->status['dv'] . PHP_EOL; 36 | $cmd_id += 1; 37 | sleep(2); 38 | 39 | echo PHP_EOL . date('H:i:s', time()) . PHP_EOL; 40 | echo $bulb->getInfo($cmd_id) . PHP_EOL; 41 | $cmd_id += 1; 42 | sleep(2); 43 | 44 | echo PHP_EOL . date('H:i:s', time()); 45 | if($bulb->powerOn($cmd_id)) echo ' Лампа включена.' . PHP_EOL; 46 | else echo " Лампа не включена. Ошибка: $bulb->error" . PHP_EOL; 47 | $cmd_id += 1; 48 | sleep(2); 49 | 50 | echo PHP_EOL . date('H:i:s', time()); 51 | if($bulb->powerOff($cmd_id)) echo ' Лампа выключена.' . PHP_EOL; 52 | else echo " Лампа не выключена. Ошибка: $bulb->error" . PHP_EOL; 53 | $cmd_id += 1; 54 | sleep(2); 55 | 56 | for ($i = 1; $i < 5; $i++) { 57 | echo PHP_EOL . date('H:i:s', time()); 58 | if($bulb->setScene($i, $cmd_id)) echo " Включена сцена $i." . PHP_EOL; 59 | else echo " Сцена $i не выключена. Ошибка: $bulb->error" . PHP_EOL; 60 | $cmd_id += 1; 61 | sleep(2); 62 | } 63 | 64 | echo PHP_EOL . date('H:i:s', time()); 65 | if($bulb->powerOff($cmd_id)) echo ' Лампа выключена.' . PHP_EOL; 66 | else echo " Лампа не выключена. Ошибка: $bulb->error" . PHP_EOL; 67 | 68 | } else echo " Статус лампы не получен. Ошибка: $bulb->error" . PHP_EOL; 69 | --------------------------------------------------------------------------------