Умный дом на openHAB+MQTT+Arduino. Часть 1: Кластер

Изначально была мысль повысить доступность openHAB средствами виртуализации. Ставим два гипервизора, настраиваем High availability, при отказе хоста виртуалка с openHAB перезапустится на соседнем сервере. И все бы ничего, но для работы HA нужно общее хранилище. Какой-то NAS допустим у меня есть, но выход его из строя даже более вероятен, чем отказ хоста. А городить что-то на DRBD или подобном не хотелось. Поэтому было решено кластеризовать openHAB другим способом, см. рисунок ниже. 

a25a2e5fe244e02d085672367cdd493a.png

Идея

Разворачиваются две виртуалки. Также можно использовать физические машины, разные малинки и т.п. В качестве дистрибутива я использовал CentOS 9 Stream, но это не принципиально. Настраиваем keepalived для перекидывания IP-адреса с одной машины на другую.  Настраиваем lsyncd для синхронизации данных openHAB, поднимаем mosquitto и, кажется, все. Теперь подробнее.

Установка openHAB

Процесс установки описан тут — https://www.openhab.org/docs/installation/linux.html.

Если вкратце, то для CentOS 9 Stream создаем файл /etc/yum.repos.d/openhab.repo 
С таким содержимым:  


[openHAB-Stable]  
name=openHAB Stable  
baseurl=https://openhab.jfrog.io/artifactory/openhab-linuxpkg-rpm/stable  
gpgcheck=1  
gpgkey="https://openhab.jfrog.io/artifactory/api/gpg/key/public"  
enabled=1

Затем делаем:

# dnf update

# dnf install openhab

# dnf install openhab‑addons

# dnf install java-17-openjdk

Если есть другие инсталляции java, то их нужно удалить, например:  
# dnf remove java-11-openjdk 

Либо запустить команду:  

# alternatives --config java 
И выбрать 17ю версию. 
Запускаем и добавляем в автозагрузку:

#dnf systemctl start openhab 

#dnf systemctl enable openhab 

Интерфейс будет доступен по адресу http://IP:8080 

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

Установка и настройка keepalived 

На обоих серверах пропишем имена и адреса в файл /etc/hosts 

10.20.10.41 srv-oh-01
10.20.10.42 srv-oh-02

Теперь нужно установить и настроить VRRP на обоих нодах:  

# dnf install keepalived  

Вот такой конфигурационный файл получился/etc/keepalived/keepalived.conf:
 
vrrp_instance failover_oh {  
state BACKUP  
interface ens192  
virtual_router_id 10  
priority 100  
advert_int 1  
preempt_delay 30  
authentication {  
auth_type AH  
auth_pass active-pass  
}  
notify /etc/keepalived/oh.sh  
 
unicast_peer {  
   10.20.10.42  
}  
  virtual_ipaddress {  
       10.20.10.40 dev ens192 label ens192:vip   
  }  
}
 

На обоих нодах конфигурация одинакова, за исключением unicast_peer, на втором сервере, он будет 10.20.10.41. 

Прошу заметить, что на обоих серверах прописано «state BACKUP», а приоритет тоже одинаков. Это делает ноды равнозначными и какой сервер раньше загрузился, тот и будет мастером, до момента отказа. 

Кроме переключения IP-адреса мы так же выполняем notify-скрипт, который на самом деле запускает и останавливает openHAB, чтобы он не обращался ко всяким внешним биндингам (например Telegram) с двух нод одновременно, вот этот файл:  

#!/bin/bash  
TYPE=$1  
NAME=$2  
STATE=$3  
 
case $STATE in  
       "MASTER") systemctl start openhab  
                 ;;  
       "BACKUP") systemctl stop openhab  
                 ;;  
       "FAULT")  systemctl stop openhab  
                 exit 0  
                 ;;  
       *)        /usr/bin/logger "oh unknown state"  
                 exit 1  
                 ;;  
esac
  

Запускаем и добавляем в автозапуск keepalived 

# systemctl start keepalived
# systemctl enable keepalived

Можно запустить на обоих нодах:  

# journalctl -af 
И посмотреть, что будет в логах при start/stop keepalived на каждой ноде:  

Hidden text

Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: (failover_oh) Backup received priority 0 advertisement
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: (failover_oh) Receive advertisement timeout
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: (failover_oh) Entering MASTER STATE
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: (failover_oh) setting VIPs.
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: (failover_oh) Sending/queueing gratuitous ARPs on ens192 for 10.20.10.40 
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:15 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:15 srv‑oh-01 systemd[1]: Started openHAB — empowering the smart home.
Jan 24 11:32:20 srv‑oh-01 Keepalived_vrrp[924]: (failover_oh) Sending/queueing gratuitous ARPs on ens192 for 10.20.10.40 
Jan 24 11:32:20 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:20 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:20 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:20 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40 
Jan 24 11:32:20 srv‑oh-01 Keepalived_vrrp[924]: Sending gratuitous ARP on ens192 for 10.20.10.40

Я остановил keepalived на srv-oh-02, теперь первый стал мастером, установился vIP (10.20.10.40), отправились ARP-пакеты, чтобы побыстрее обучить коммутатор и запустился openHAB. 

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

Установка и настройка lsyncd 

В openHAB конфиги лежат в /etc/openhab, а также две базы — rrd4 (где хранятся метрики) и jsondb (где хранятся так называемые семантические элементы). Для синхронизации этих данных будем использовать lsyncd, который позволяет (при помощи rsync) сихронизировать файлы сразу после их создания или изменения, причем в обе стороны, т.е. сервер где модификация произошла позже, считается источником. 

Для работы lsyncd надо сгенерировать ключи SSH, на одном из серверов, например на 1-м делаем:  

# ssh-keygen
# ssh-copy-id srv-oh-02

Первая команда сгенерирует закрытый и публичный ключ, вторая скопирует публичный ключ на удаленный сервер, теперь на него можно заходить без пароля. Такую же процедуру нужно провезти на 2-м сервере, я просто скопировал этот же ключ в .ssh/authorized_keys 

Теперь установим lsyncd на оба сервера:  

# dnf install lsync 

Напишем конфигурационный файл /etc/lsyncd.conf, на первом сервере он будет такой:  

settings {  
       logfile = "/var/log/lsyncd/lsyncd.log",  
       statusFile = "/var/log/lsyncd/lsyncd.status",  
       statusInterval = 1,  
       nodaemon = true,  
       insist = true  
}  
sync {  
       default.rsync,  
       source = "/etc/openhab/",  
       target = "srv-oh-02:/etc/openhab",  
       delete = 'running',  
       --delay = 5,  
       rsync = {  
               -- timeout = 3000,  
               update = true,  
               _extra={"--temp-dir=/temp-lsync/"},  
               times = true,  
               archive = true,  
               compress = true,  
               perms = true,  
               acls = true,  
               owner = true,  
               verbose = true  
       }  
}  
sync {  
       default.rsync,  
       source = "/var/lib/openhab/persistence",  
       target = "srv-oh-02:/var/lib/openhab/persistence",  
       delete = 'running',  
       --delay = 5,  
       rsync = {  
               -- timeout = 3000,  
               update = true,  
               _extra={"--temp-dir=/temp-lsync/"},  
               times = true,  
               archive = true,  
               compress = true,  
               perms = true,  
               acls = true,  
               owner = true,  
               verbose = true  
       }  
}  
sync {  
       default.rsync,  
       source = "/var/lib/openhab/jsondb",  
       target = "srv-oh-02:/var/lib/openhab/jsondb",  
       delete = 'running',  
       --delay = 5,  
       rsync = {  
               -- timeout = 3000,  
               update = true,  
               _extra={"--temp-dir=/temp-lsync/"},  
               times = true,  
               archive = true,  
               compress = true,  
               perms = true,  
               acls = true,  
               owner = true,  
               verbose = true  
       }  
}
 

Каждая секция sync настраивается отдельно. 

На втором сервере конфигурация такая же, только в target«ах надо изменить имя сервера на srv-oh-01. Еще нужно на обоих нодах создать директорию /temp-lsync, для хранения временных данных. 

Запускаем его и добавляем в автозагрузку на обоих нодах:  

# systemctl start lsyncd
# systemctl enable lsyncd

Логи можно смотреть так:
# tail -f /var/log/lsyncd/lsyncd.log 

Hidden text

Jan 24 11:32:10 srv-oh-02 lsyncd[857]: 11:32:10 Normal: Calling rsync with filter-list of new/modified files/dirs  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Gas.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gGas.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gTemperature.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Motion.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Button_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Water_2.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Shutter_3.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Water_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Shutter_2.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Shutter_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Temperature.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Shutter_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gWater.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Humidity.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/Security.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/OU_Toilet_Light.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gLight.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/OU_Temperature.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Door_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Light.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Light_2.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Light_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gShutter.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Gas.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gMotion.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gWindow.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Window_1.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Motion.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_Kitchen_Temperature.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Shutter_4.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Shutter_3.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/GF_FamilyRoom_Shutter_2.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gHumidity.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/gDoor.rrd  
Jan 24 11:32:10 srv-oh-02 lsyncd[857]: /rrd4j/Day.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: sending incremental file list  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/Day.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Gas.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Humidity.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Light.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Motion.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Shutter_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Shutter_2.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Shutter_3.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Shutter_4.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_FamilyRoom_Temperature.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Button_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Door_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Gas.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Light_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Light_2.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Motion.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Shutter_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Shutter_2.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Shutter_3.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Temperature.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Water_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Water_2.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/GF_Kitchen_Window_1.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/OU_Temperature.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/OU_Toilet_Light.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/Security.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gDoor.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gGas.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gHumidity.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gLight.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gMotion.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gShutter.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gTemperature.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gWater.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: rrd4j/gWindow.rrd  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: sent 12,743 bytes  received 161,256 bytes  115,999.33 bytes/sec  
Jan 24 11:32:11 srv-oh-02 lsyncd[1753681]: total size is 20,477,492  speedup is 117.69  Jan 24 11:32:11 srv-oh-02 lsyncd[857]: 11:32:11 Normal: Finished a list after exitcode: 0

Установка и настройка MQTT-брокера

я выбрал один из популярных брокеров — Mosquitto.  

Установка:  

# dnf install epel-release
# dnf install mosquitto 

В конфигурационный файл (/etc/mosquitto/mosquitto.conf) нужно добавить password_file, чтобы получилось так:

listener 1883 
log_type all 
log_dest file /var/log/mosquitto/mosquitto.log
log_type all
password_file /etc/mosquitto/users.txt
 

Создаем пользователя openhab:  

# mosquitto_passwd -c /etc/mosquitto/users.txt openhab

Остальные пользователи добавляются так:  

# mosquitto_passwd -b /etc/mosquitto/users.txt user1 password 
установим права на файл:  

# chmod 644 /etc/mosquitto/users.txt 

Подробности по управлению пользователями тут — https://mosquitto.org/man/mosquitto_passwd-1.html 

Запускаем и добавляем его в автозапуск:  

# systemctl start mosquitto
# systemctl enable mosquitto

По аналогии устанавливаем mosquitto на второй сервер.

Устанавливаем MQTT Binding в openHAB

Заходим на веб-интерфейс и идем в Add-on Store→Bindings→MQTT Binding, жмем Install. Подключаем MQTT bridge сдедующим образом: Settings→Things→ жмем СИНИЙ ПЛЮС→MQTT Binding→ MQTT Broker.

Заполняем поля:

Unique ID (Должен сгенерироваться автоматически, но можно и самому придумать);
Label;
Location;
Broker Hostname/IP;

Устанавливаем галочку «Show advanced» и далее заполняем:
Username;
Password;
После чего жмем »Create Thing»

45b3714347b4367d29b341b532d17e39.png

В списке Things должен появиться MQTT Broker и статус должен стать Online.

fa027ae6f285a30e97e7d4a75e4e0b67.png

Жмем опять СИНИЙ ПЛЮС, выбираем MQTT Binding и затем Generic MQTT Thing.

1fd2f12cc98fd0c445d1dba6b9039a5e.png

Тут выбираем Bridge который добавили ранее и жмем «Create Thing». Вот что должно получиться в результате. 

9c6fbfe17cf3ee91917e90afbadc8685.png

Добавим канал. Жмем на появившийся Generic MQTT Thing и переходим во вкладку Channels

Теперь нажимаем «Add Channel»

ade7ffb7d7e310810b4b23434e5aa542.png

Например, нам надо добавить канал для температуры в кухне, заполняем Channel Identifier (надо придумать), Label, можно еще указать Description, теперь выбираем тип «Number», откроются дополнительные поля:  

3329fc291d82f82d37271783f5a4e006.png

Тут заполняем «MQTT State Topic», ставим галочку «Show advanced» 

5a30133b41f5c7b5e55468abafd9c24f.png

Устанавливаем единицы измерения — градус Цельсия.

b2995fb3f8a0ea0c3d80411544ffc4a2.png

А также желаемый формат вывода, так будет выводиться точность до десятых градуса. Жмем «Create»

Предположим, у нас уже есть созданный Item такого плана:  

Number   GF_Kitchen_Temperature  «Температура [%.1f]%unit%»        (GF_Kitchen, gTemperature)    [«Temperature»] 

Тогда жмем на создавшейся канал, теперь на маленький зеленый плюсик «Add link to Item», откроется такое окошко:  

0cab12df60b37d6b8b5a73d167c96d74.png

Тут можно создать новый Item, но у нас он уже существует, поэтому жмем «Item to Link», выбираем из списка нужный и жмем «Link»

Все, теперь если кто-то отправил в канал oh/kitchen/temp значения температуры, то openHAB его оттуда возьмет. Получится вот такой график.

5ef04951fe7d6ea08d1ffd79887711e1.png

MQTT Explorer как инструмент отладки

Однако у нас пока нет настроенного оборудования, которое бы это делало, поэтому предлагаю проверить работу при помощи MQTT Explorer, устанавливаем отсюда — http://mqtt-explorer.com/ 

e95f4893922646548217f0ef500066d5.png

Подключаемся и можем читать/писать топики.

40ee4d2b2b764380c86405a6bb3c692d.png

У меня была мысль поставить EMQX и настроить кластерный MQTT, или поднять mosquitto в режиме моста и как-то синхронизировать два сервера. Но оказалось, что данных репликации openHAB  через lsyncd достаточно. Топики быстро переписываются и хранить данные на обоих MQTT-брокерах не обязательно, но можно. 

Заключение

Вот таким образом удалось кластеризовать openHAB. В следующей статье мы соберем модуль умного дома на Arduino, подключим Ethernet, подключимся к MQTT-брокеру и сможем отправлять/получать данные, управлять реле и т.п.

© Habrahabr.ru