Тащим данные с НОВОГО кабинета het.uz в умный дом

Ранее, я писал статью о том, как спарсить баланс с личного кабинета потребителя электроэнергии. Тот, кто пользовался предыдущей версией парсера (как и самого кабинета) наверняка заметил, что парсинг работает с переменным успехом и жуткими тормозами. И вот, как гром средь бела дня, HET наконец меняет этот кусок засохшего г… кхм… короче! У них там новый кабинет. Если эту статью читают разработчики этого нового кабинета – салют вам и моё почтение! А всем остальным спешу сообщить радостную новость: в виду того, как этот новый кабинет построен, парсить больше ничего не нужно! Потому, что теперь данные можно по-человечески забирать прямо из API, с которым новый кабинет, собственно, сам и работает. Лёгким нажатием на F12 можно отследить, что, как и откуда забирает JS приложение. Данные, кстати, обновляются весьма резво. Так, закинутая на счёт оплата практически моментально появляется в кабинете. Но, как бы красив и удобен не был этот кабинет, мне нужно немного большее. И в этот раз, я решил сделать так, чтобы данные сразу попадали в кровеносную систему умного дома – то есть, прямо в MQTT.

В первую очередь, нам понадобится библиотека mqtt-клиента для php. Я взял первый попавшийся вариант вот тут https://github.com/php-mqtt/client и он мне вполне устроил.

Забегаем в папку, где будет располагаться наш скрипт и устанавливаем библиотеку

composer require php-mqtt/client

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

<?php
	define('HET_LOGIN',"номер счёта");
	define('HET_PASS',"Пароль");
	
	$server   = 'хост MQTT брокера';
	$port     = 1883;
	$clientId = 'het-informer';
	$mqttLogin = "логин от mqtt";
	$mqttPass = "пароль от mqtt";
	// $balancefile = "dom.txt"; 
        // Раскомментируй строку выше, если хочешь кешировать данные о балансе, чтобы можно было забирать их как статику. В моём случае это нужно для работы навыка Алисы.
	require_once ('het.php');

Следующий файл называется het.php

<?php
	if(!defined('HET_LOGIN')){
		die();
	}

	require_once 'vendor/autoload.php'; //Подгружаем библиотеку mqtt
	
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); 
	curl_setopt($ch, CURLOPT_TIMEOUT, 5);
	curl_setopt($ch, CURLOPT_URL, 'https://cabinet-api.het.uz/household-consumer/v1/mobile-cabinet/user-login'); // Тут у нас адрес для авторизации. Мы должны отправить туда json массив с учётными данными.
	curl_setopt($ch, CURLOPT_USERAGENT,'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36'); // Прикидываемся браузером, чтобы не спровоцировать какой-нибудь фильтр. Раньше у них такой стоял...
	curl_setopt($ch, CURLOPT_POST, true); // отправлять будем методом POST
	curl_setopt($ch, CURLOPT_POSTFIELDS, '{"login":"'.HET_LOGIN.'", "password":"'.HET_PASS.'"}'); // Собственно, массив с учётными данными
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_HTTP09_ALLOWED, true);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
	$headers = array(
	   "Connection: keep-alive",
	   "Keep-Alive: timeout=5, max=100",
	   "Content-Type: application/json", // вот тут обязательно указываем тип данных, иначе API пошлёт нас нафиг.
	);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	$authfile = '/tmp/el'.HET_LOGIN.'.json'; // тут мы создаём файл, куда будем прятать токен авторизации. Попытки постоянных повторных автоиризаций во время тестирования завели меня во временный бан. Больше так не делаем. :)

	if(file_exists($authfile)){
		if($cache = file_get_contents($authfile)){
			if(stristr($cache,'Successfully')){
				$answer = $cache;
				$cache = json_decode($cache, true);
				if($cache["timestamp"]+$cache["data"]["expiresIn"] > time()){ // Проверяем актуальность токена, взятого из файла. При любой непонятной ситуации включаем переавторизацию. 
					$refresh = false;
				} else {
					$refresh = true;
				}
			} else {
				$refresh = true;
			}
		} else {
			$refresh = true;
		}
	} else {
		
		$refresh = true;
	}

	if($refresh == true){
		echo "REFRESH\r\n";
		$answer = curl_exec($ch);
		if (curl_error($ch)) {
			echo curl_error($ch);
		}
		if(stristr($answer,'Successfully')){
			file_put_contents('/tmp/el'.HET_LOGIN.'.json',$answer);
		}
	}
	if(stristr($answer,'Successfully')){
		$answer = json_decode($answer, true);
	} else {
                unlink($authfile); // Если что-то пошло не так, грохаем кеш авторизации и завершаем работу скрипта, ибо дальше в ней нет смысла.
		die('unsuccess');
	}
			
	$token = $answer["data"]["accessToken"]; // Ну так, для наглядности. Конечно, модно было не плодить переменные, но чё нам, жалко памяти? ;)

	curl_setopt($ch, CURLOPT_URL, 'https://cabinet-api.het.uz/household-consumer/v1/mobile-cabinet/consumer-state'); // Запрашиваем данные со нужного раздела API
	curl_setopt($ch, CURLOPT_POST, false);
	$headers = array(
	   "Connection: keep-alive",
	   "Keep-Alive: timeout=5, max=100",
	   "Authorization: Bearer ".$token, //Добавляем токен авторизации в заголовок нового запроса.
	   "Coato-Code: ".$answer["data"]["coatoCode"], //Тут требуется добавить номер РЭС. Берётся так-же из ответа на авторизацию.
	);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	$answer = curl_exec($ch); // Пуляем запрос

	if (curl_error($ch)) {
		echo curl_error($ch);
	}
	curl_close($ch);
	print_r($answer); // Смотрим, что прилетело. В принципе, это можно закомментить
	if(stristr($answer,'Successfully')){
		
		$answer = json_decode($answer,true); // Превращаем JSON в обычный массив
		$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId); // создаём экземпляр для MQTT
		
		if(isset($balancefile))
			file_put_contents($balancefile,$answer["data"]["balance"] / 100); // Складываем данные о балансе в статический файл, если включено в настройках.
		
		$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
			->setConnectTimeout(3)
			->setUsername($mqttLogin)
			->setPassword($mqttPass); // тут у нас добавляются данные для авторизации в mqtt. Если у вас там всё открыто (чего я категорически не советую), можно это выпилить и сделать $mqtt->connect() без аргументов.
		
		$mqtt->connect($connectionSettings, true);
		if($answer["data"]["balance"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/balance', $answer["data"]["balance"] / 100, 1);
		if($answer["data"]["lastCrawlReading"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/lastCrawlReading', $answer["data"]["lastCrawlReading"] / 1000, 0);
		if($answer["data"]["lastCrawlDate"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/lastCrawlDate', $answer["data"]["lastCrawlDate"], 0);
		if($answer["data"]["lastPayment"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/lastPayment', $answer["data"]["lastPayment"] / 100, 0);
		if($answer["data"]["lastPaymentDate"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/lastPaymentDate', $answer["data"]["lastPaymentDate"], 0);
		if($answer["data"]["currentMonthCalcKwh"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/currentMonthCalcKwh', $answer["data"]["currentMonthCalcKwh"] / 1000, 0);	
		if($answer["data"]["currentMonthCalcSum"]!==null)
			$mqtt->publish('het/'.HET_LOGIN.'/currentMonthCalcSum', $answer["data"]["currentMonthCalcSum"] / 100, 0);
		$mqtt->publish('het/'.HET_LOGIN.'/ecoCurrentMonthCalcKwh', $answer["data"]["ecoCurrentMonthCalcKwh"]/1000, 0);

// Значения, которые по идее, должны быть дробными приходят целыми числами, по этому приходится делать соответствующие деления. Ну, либо можно оставить это на откуп узлам умного дома. Но мне было проще вот так. В некоторых случаях API отдаёт значение null, что в результате роняет библиотеку mqtt, по этому делаем проверку и не отправляем ничего со значением null. 
		$mqtt->disconnect(); // Закрываем соединение
	} else {
		unlink($authfile); // Если в ответ на запрос данных пришло не то, что нужно, грохаем кеш авторизации, потому, что причина скорее всего в ней.
	}
?>

Если по какой-то причине что-то поломало авторизацию, скрипт запросит её снова при следующем запуске. Я не стал делать рекурсивных итераций, потому, что это может привести к бану на стороне API. Скрипт можно запускать как через cron, так и закинуть на веб сервер, и вызывать через автоматизацию в “умном доме”.

P.S.
Разработчикам нового кабинета огромный респектище! Я ещё пять лет назад пытался выпросить у het.uz API для получения данных, но все мои письма были проигнорированы.

Для чего вообще забирать эти данные к себе? К примеру, я сделал автоматическое оповещение в семейную телеграм группу, если на балансе домашнего или дачного счётчика осталось меньше 30к сум. Я и вовсе хотел сделать автоматическую генерацию счёта к оплате, но пока что, ни одна платёжная система не захотела помочь мне в реализации такой идеи. Надеюсь, когда нибудь я дождусь и этого. :)