Умный дом на openHAB+MQTT+Arduino. Часть 1: Кластер
Изначально была мысль повысить доступность 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
:
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»
В списке 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%»
Тогда жмем на создавшейся канал, теперь на маленький зеленый плюсик «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-брокеру и сможем отправлять/получать данные, управлять реле и т.п.