Как стать автором
Обновить

Повышение надежности контроллера умного дома на Majordomo (MQTT)

Время на прочтение5 мин
Количество просмотров11K

Итак, мой умный дом готов, слушается голоса, управляет климатом, зарядкой аккумулятора на даче (https://habr.com/ru/post/538896/).

Более того, умные устройства стоят теперь как на даче, так и дома, в городе. Причем из-за особенностей совместимости экосистем с Яндексом часть устройств дома (RGB ленты) управляются через сервер на Majordomo (дача).

И вот тут возникает ряд логичных вопросов:

  • Где должен стоять сервер – дома или на даче?

  • Потерей управления какими устройствами жертвовать при обрыве связи между домом и дачей?

  • Как не грузить GSM канал до дачи передачей графиков в HTML верстке сайта?

Легко догадаться, что ответом является резервирование:

  1. Серверы должны быть и там и там

  2. Серверы должны уметь управлять всеми устройствами

  3. Серверы должны иметь полный набор данных

Так как датчики общаются с сервером в основном через протокол MQTT, MQTT брокер так же становится точкой отказа.

Резервирование сервера

Начнем с MQTT брокера. Если не считать таких сообщений, как LWT («последняя воля устройства») и Retain (хранимых на сервере), большинство сообщений передаются одномоментно и только тем, кто в данный момент подключен к брокеру. То есть "отправил - забыл".

К счастью, в последних версиях mosquitto сервера есть режим бриджа – вы просто задаете адрес второго брокера и топики, которые нужно дублировать, направления дублирования. В моем случае вполне пригодился вариант «копировать все в обе стороны». Вот как это делается в raspbian/armbian – добавляем в /etc/mosquito/mosquito.conf:

#connection bridge-01
connection bridge-01
address mqtt.mydomain.ru:1883
topic # out 0
topic # in 0

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

Дальше, сами серверы Majordomo. Я сделал второй сервер на базе Orange pi one plus (1Gb RAM) – стоит в 2 раза дешевле Raspberry Pi4, для вспомогательного сервера - то, что надо. Но серверы должны уметь делать одно и то же, но не делать этого одновременно (в большинстве случаев это не страшно, ну поступит 2 команды на включение зарядника – не страшно, но некоторые вещи лучше дважды не делать, например, не поворачивать солнечные панели по часам).

Так как для корректной работы с датчиками и исполнительными устройствами я использую MQTT, логично отслеживать работоспособность удаленного сервера через тот же MQTT. Для этого я создал отдельный класс, в котором есть 2 статуса (для отображения) и 2 времени – для локального сервера и для удаленного, а также адрес активного брокера и собственный адрес сервера. Раз в 10 секунд выполняется проверка системного цикла MQTT – время последнего запуска (ThisComputer.cycle_mqttRun). Это время сравнивается с текущим (time()). Если прошло больше 10 секунд – паникуем, то есть понимаем, что локальный сервер не дружит с MQTT брокером и показываем это в интерфейсе. Так же сравниваем время последнего запуска MQTT цикла на удаленном сервере (приходит через MQTT). Если прошло больше 20 секунд, а локальный цикл в порядке – понимаем, что удаленный сервер больше не управляет устройствами. Проверяем еще один параметр, передаваемый через MQTT – имя активного брокера. Если это не локальный, то надо переключать на себя:

$val=getGlobal("ThisComputer.cycle_mqttRun");
$locval=time()-$val;
$this->setProperty("LocValue",$val);
$this->setProperty("LocDeltaT",$locval);
if($locval>10)
	$locstate=1;
else
  $locstate=0;
$tmp=$this->getProperty("Status");
if(is_null($tmp))
	$tmp=10;
if($tmp!=$locstate)
	$this->setProperty("Status",$locstate);
$remval=time()-$this->getProperty("RemValue");
$newstate=($remval<20)?0:1;
$this->setProperty("RemStatus",$newstate);
$ot = $this->object_title;
$currBroker=$this->getProperty("MQTT_broker");
$sA=$this->getProperty("selfAddress");
if($sA!=$currBroker)
	$this->setProperty("isController",0);
setTimeOut($ot . "_checkCycle",'callMethod("'.$ot.'.checkCycle");',10);
if(
	(!$locstate&&($newstate||($this->getProperty("LinkedRoom")=="Energoblok")))&&
	($sA!=$currBroker)
)// remote failed local good or local is good and is not local server
{
	debMes('Switch to '.$this->getProperty("selfAddress"),0);
	$cnt=0;
	for($i=40;$i<90;$i++)
	{
		if(ping('192.168.3.'.number_format($i,0)))
    {
			getURL('http://192.168.3.'.number_format($i,0).'/cm?cmnd=MqttHost%20'.$this->getProperty("selfAddress"));
			debMEs('http://192.168.3.'.number_format($i,0).' is online',0);
			$cnt++;
			$this->setProperty("LocValue",time());
		}
	}
	if($cnt>10)
	{
		$this->setProperty("MQTT_broker",$this->getProperty("selfAddress"));
		$this->setProperty("isController",1);
	}
}
Вот такой виджет, пока корявенько, зато информативно
Вот такой виджет, пока корявенько, зато информативно

У меня Tasmota устройства (IP в диапазоне c 192.168.3.40 по 192.168.3.90), им можно передать обычным URL запросом новый адрес MQTT сервера. Вот только запросы надо посылать синхронные, а главное – не забывать между ними обновлять MQTT свойство для удаленного сервера. Иначе получится замкнутый цикл – начали переключаться, больше 10 секунд не сообщаем удаленному серверу, что мы живы. Тот начинает переключение на себя и тоже замирает. Не делайте так.

Повышаем надежность самого сервера

Операционная система и БД хранятся на карте памяти. Есть карты класса А1 и даже А2, но через год постоянной нагрузки такая карта с большой вероятностью загнется. Кроме того, штатный код пишет в базу кучу ненужного, а каждое чтение/запись свойства любого объекта – это обращение к БД. У меня было порядка 1200 обращений к БД в секунду.

Карту можно спасти, если базу держать в оперативной памяти. К счастью, разработчики Majordomo сделали прошивку для Raspberry сразу с опцией БД в памяти, а для прочих платформ есть скрипт для переноса БД в память (но памяти должно быть не менее 1Гб, на orange pi zero c 512Мб у меня не взлетело - база весит порядка 300Мб и столько же надо дополнительно для дампов бэкапов). Да, теперь в любой момент просто так перезагружать систему нельзя, нужно выполнить скриптик, иначе данные за последние полчаса потеряются (но база всегда бэкапится в рабочем состоянии!). Зато скорость работы БД и долговечность карты памяти – просто великолепны.

Остался последний штрих – снизить нагрузку на БД, убрав лишние запросы. Решение простое:

  • Обновиться до последней версии (буквально пару недель назад обновили интерфейс, убрав лишние обращения к БД и переписав на java скрипты)

  • Обращаться к локальным или глобальным свойствам только один раз и использовать переменные в памяти (смотрите на пример кода цикла проверки – getProperty\setProperty там использованы только по одному разу).

Еще пример оптимизации скриптов – чтобы и обращений лишних не было, и чтобы срабатывала автоматическая зависимость от переменной, например:

if((($temp2Floor=getGlobal("sTemp2Floor.value"))<'21')&&
	gg("remote_mqtt_updated.isController")) // if remote failed
{
		if ($temp2Floor < '21' && !getGlobal("rConserveSW.status") && timeBetween('2:00', '8:00')) 
		{
  		if (!getGlobal("rDieselHome.status")) 
			{
   	 		callMethod("rDieselHome.turnOn");
  		}
		} else if ($temp2Floor > '23') 
		{
  		if (getGlobal("rDieselHome.status")) 
			{
    		callMethod("rDieselHome.turnOff");
  		}
		}
}

Обратите внимание, что в первом условии есть ветка проверки, этот ли сервер управляет устройствами в настоящий момент (gg("remote_mqtt_updated.isController")). remote_mqtt_updated – это объект контроля работы серверов.

Теперь у меня число обращений к БД порядка 380 в секунду, что гораздо лучше по сравнению с начальным значением в 1200.

Итог

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

Теги:
Хабы:
+1
Комментарии14

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн