Pull to refresh

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

Level of difficultyMedium
Reading time14 min
Views4.9K

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

Идея

Разворачиваются две виртуалки. Также можно использовать физические машины, разные малинки и т.п. В качестве дистрибутива я использовал 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:

Hidden text

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, на первом сервере он будет такой: 

Hidden text

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
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 600 /etc/mosquitto/users.txt

# chown mosquitto:mosquitto /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"

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

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

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

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

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

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

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

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

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

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

Number   GF_Kitchen_Temperature  "Температура [%.1f]%unit%"    <temperature>    (GF_Kitchen, gTemperature)    ["Temperature"] 

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

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

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

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

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

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

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

Заключение

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

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments9

Articles