Главная » Хабрахабр » Использование rrd4j для OpenHab2 persistence

Использование rrd4j для OpenHab2 persistence

OpenHab – популярный сервер «умного дома» (или IoT, как сейчас модно говорить) и уже обозревался на Хабре. Тем не менее, документации по отдельным аспектам настройки сервера не так много, как хотелось бы. А на русском её, считай что и нет.

Важной особенностью OpenHab является модульность. Сам по себе сервер обеспечивает базовые функции (даже без какого бы то ни было UI). Весь остальной функционал предоставляется плагинами. Одним из типов плагинов является persistence – предоставление возможности хранить историю значения для айтемов (параметров устройств). Это необходимо для отображения исторических данных (графики) и восстановления состояния айтемов при рестарте сервера.

Существующие плагины позволяют использовать для хранения все популярные БД. Я же расскажу про настройку очень интересного бекэнда – rrd4j. Это высокопроизводительное хранилище для данных, которые представляют собой ряды значений, привязанных ко времени. Автор вдохновлялся набором RRDTools, но переписал его функционал на Java (OpenHab тоже написан на Java), оптимизировал и расширил функционал. Файлы хранилищ rrd4j не совместимы с файлами RRDTools.
Тут нужно рассказать, что представляет собой это хранилище. Это один или несколько циклических буферов, которые хранят значения параметра с привязкой ко времени. При этом, есть возможность агрегировать несколько изменений в одно значение буфера (например, снимать данные каждую минуту, а хранить одно среднее значение за 5 мин).

Итак, имеем установку OpenHab2 без persistence. Для тестов я буду использовать вот такую плату:

На плате есть два термометра Dallas 18B20 и кнопка.

Для коммуникации с сервером она использует MySensors. Кстати, плагина для протокола MySensors, почему-то, нет в репозитории, поэтому его нужно ставить руками, скачав из темы на форуме и положив в папку /usr/share/openhab2/addons/.

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

Прошивка для платы

// Enable debug prints to serial monitor
#define MY_DEBUG // Enable and select radio type attached
#define MY_RADIO_NRF24 // Static node id
#define MY_NODE_ID 1 #include <MySensors.h>
#include <DallasTemperature.h> #define SKETCH_NAME "Test board"
#define SKETCH_MAJOR_VER "1"
#define SKETCH_MINOR_VER "0" // Item IDs
#define CHILD_ID_BTN 3
#define CHILD_ID_TEMP 10 // Pin definitions
#define PIN_BTN 2
#define PIN_BTN_LED 3
#define PIN_ONE_WIRE 7 // Messages
MyMessage msgBtn(CHILD_ID_BTN, V_STATUS);
MyMessage msgTemp(CHILD_ID_TEMP, V_TEMP); //OneWire temperature
OneWire oneWire(PIN_ONE_WIRE);
DallasTemperature temp(&oneWire);
#define MAX_TEMP_SENSORS 10
int cntSensors = 0; // Don't support hotplug
float arrSensors[MAX_TEMP_SENSORS];
DeviceAddress arrAddress[MAX_TEMP_SENSORS]; void before()
{ temp.begin();
} void presentation()
{ // Send the sketch version information to the gateway and Controller sendSketchInfo(SKETCH_NAME, SKETCH_MAJOR_VER "." SKETCH_MINOR_VER); // Present locally attached sensors present(CHILD_ID_BTN, S_BINARY); cntSensors = temp.getDeviceCount(); if (cntSensors > MAX_TEMP_SENSORS) cntSensors = MAX_TEMP_SENSORS; Serial.print("Temperature sensors found:"); Serial.print(cntSensors); Serial.println("."); for (int i = 0; i < cntSensors; i++) { present(CHILD_ID_TEMP + i, S_TEMP); arrSensors[i] = 0; temp.getAddress(arrAddress[i], i); }
} void setup()
{ // Setup locally attached sensors pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_BTN_LED, OUTPUT); temp.setWaitForConversion(false); temp.setResolution(12); // 0,0625 grad celsius
} void loop()
{ // Send locally attached sensor data here static unsigned long owNextConversTm = 0; if (0 == owNextConversTm) { temp.requestTemperatures(); int16_t owConversTm = temp.millisToWaitForConversion(temp.getResolution()); owNextConversTm = millis() + owConversTm + 50; // Wait a little more } else if (owNextConversTm < millis() && temp.isConversionComplete()) { for (int i = 0; i < cntSensors; i++) { if (temp.validFamily(arrAddress[i])) { int t1 = temp.getTempC(arrAddress[i]) * 10; float t = t1 / 10 + (t1 % 10) * 0.1; if (t != arrSensors[i]) { send(msgTemp.setSensor(CHILD_ID_TEMP + i).set(t, 1)); arrSensors[i] = t; Serial.print("Sensor #"); Serial.print(i); Serial.print(", temperature: "); Serial.print(t); Serial.println(" C."); } } } owNextConversTm = 0; } else if ((owNextConversTm + 30000) < millis()) { // It was couter reset owNextConversTm = 0; } static int currVal = HIGH; int val = digitalRead(PIN_BTN); if (val != currVal) { digitalWrite(PIN_BTN_LED, val == HIGH ? LOW : HIGH); send(msgBtn.set(val == LOW)); currVal = val; Serial.print("Btn state changed to "); Serial.println(val == HIGH ? "off." : "on."); }
}

Да, я предпочитаю использовать ClassicUI (а UI в OpenHab – это тоже плагины). Мне хочется добавить график для двух термодатчиков (видеть их на одном графике).

Для начала нужно установить плагин для rrd4j. К счастью, он есть в репозитории.

Настройка сохранения производится в файле /etc/openhab2/persistence/rrd4j.persist. Как можно понять из пути и имени, есть возможность сохранять разные данные в разные бекэнды т.к. для каждого из них будет свой файл с расписанием и списком айтемов.

Файл состоит из двух групп настроек – стратегии, где задаётся интервал сохранения в синтаксисе Quartz и элементы, где для айтемов или групп задаются стратегии.

Strategies
{
// Strategy name
// | Seconds
// | | Minutes
// | | | Hours
// | | | | Day of month
// | | | | | Month
// | | | | | | Day of week
// | | | | | | | Year every5sec : "0/5 * * * * ?" every15sec : "0/15 * * * * ?" everyMinute : "0 * * * * ?" every30min : "0 30 * * * ?" everyHour : "0 0 * * * ?" everyDay : "0 0 0 * * ?" default = everyChange
} Items
{ gTemperature* : strategy = everyMinute, restoreOnStartup
}

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

Сохраняем файл, он будет перечитан автоматически. Как видно, мы сохраняем значения датчиков каждую минуту. Сейчас это лучший вариант; а почему так – расскажу ниже.

Где же хранятся данные? В папке /var/lib/openhab2/persistence/rrd4j/. Там будет создано по файлу для каждого айтема, причём размер файла будет сразу таким, чтобы хранить все данные, включая архив. Это декларируется как важная фишка RRD-хранилищ — файл не растёт бесконтрольно.

root@chubarovo:/etc/openhab2# ll -h1 /var/lib/openhab2/persistence/rrd4j/
total 78K
drwxr-xr-x 2 openhab openhab 4.0K Apr 7 21:53 ./
drwxr-xr-x 5 openhab openhab 4.0K Feb 18 18:59 ../
-rwxr-xr-x 1 openhab openhab 32 Dec 18 15:44 Readme.txt*
-rw-r--r-- 1 openhab openhab 35K Apr 7 21:54 Test_temp1_soc.rrd
-rw-r--r-- 1 openhab openhab 35K Apr 7 21:54 Test_temp2_soc.rrd

Ну, раз всё хорошо, добавим графики себе в sitemap:

sitemap test label="Тестовый пример"
{ Text item=Test_button_soc label="Кнопка [MAP(ru.map):%s]" Text item=Test_temp1_soc Text item=Test_temp2_soc Chart item=gTemperature refresh=60000 period=4h
}

Вот как это выглядит на экране:

Кстати, в мобильном приложении графики также есть:

Ещё одна интересная особенность rrd4j (и оригинальных RRDTools) – встроенный движок рендеринга графиков. В OpenHab такой график можно вставить в другом UI – HabPanel. Выглядит он олдскульно:

Движок доступен и напрямую по ссылке. Она должна выглядеть примерно так:

http://192.168.144.243:8080/rrdchart.png?groups=gTemperature&theme=black&period=h&h=464&w=447

w и h – размер графика в пикселях. В параметре groups задаются названия групп для отображения. Для отдельных айтемов нужно использовать параметр items. Если нужно показать несколько групп/параметров, то их нужно указать через запятую.

Ладно, как вообще это всё работает? Как часто сохраняется значение? Сколько хранится? Да и вообще, у rrd4j масса настроек, а мы ничего не делали!

Чтобы посмотреть текущее хранилище воспользуемся графической утилитой, которая ходит вместе с плагином для OpenHab. На моём тестовом сервере нет иксов, поэтому я скопировал файлы данных и .jar плагина на основную машину с Windows.

Для запуска, конечно, понадобится Java. У неё есть прикольная особенность запускать любой подобающий класс из .jar по имени. Нам нужен org.rrd4j.inspector.RrdInspector и запускаем его вот так:

java -cp rrd4j-2.1.1.jar org.rrd4j.inspector.RrdInspector

И вот что мы увидим для нашего архива:

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

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

Как вы уже поняли, конфигурация по умолчанию нам не подходит. Так что неплохо бы её изменить. Настройки хранилища лежат в /etc/openhab2/services/rrd4j.cfg. Сейчас файл пуст, но сверху есть подсказка по синтаксису.

Итак, выше я уже писал общие слова по формату. Теперь пора заняться конфигурированием. Как видно из примера, настройки идут в две строки: .def и .archives. В параметре .items через запитую перечисляются айтемы, для которых будет применяться конфигурация. Те параметры, для которых нет своей конфигурации, будут сохраниться с настройками по умолчанию (мы их видели выше в Java-смотрелке).

Сделаем настройки для градусников:

# each 15 sec
temps.def=GAUGE,15,0,100,15
# 4h/15s : 1m/24h : 5m/7d
temps.archives=AVERAGE,0.5,1,960:AVERAGE,0.5,4,1440:AVERAGE,0.5,20,2016
temps.items=Test_temp1_soc,Test_temp2_soc

Как можно заметить, синтаксис несколько отличается от классических RRDTools. В .def мы задали тип значения GAUGE – это хранение абсолютных значений. Дальше heartbeat, минимальное, и максимальное возможные значения. И step.

Step – это нормальный интервал между отсчётами в секундах. Я выбрал 15, как и хотел.
Hearbeat добавляется к step и это максимальный интервал времени, для сохранения значения. Если за это время (step + heartbeat = 30 сек в моём случае) не будет записано значение, то rrd4j сохранит в отсчёт NaN.

В .archives задаю настройки хранения. Всё, что записывается в файл, настраивается здесь. Как можно заметить, архивов может быть несколько. Синтаксис одного архива приведён в описании в файле.

В качестве агрегатной функции я использую AVERAGE т.к. у меня температура и при объединении отсчётов нужно сохранять среднее. Дальше – параметр xff, который слабо описан в интернете. Пришлось слазить в исходники. Это оказался коэффициент (по сути, процент), который определяет, при каком количестве NaN, при группировке запишется итоговый NaN.

Например, для второго архива, где объединяются 4 значения, при коэффициенте 0,5 итоговый NaN будет записан, если два или более исходных значения – NaN.

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

В моей настройке я создал три архива с такими параметрами:

• Храним каждое значение, всего 960 штук (960 * 15 сек = 4 часа).
• Храним среднее из четырёх значений, всего 1440 штук (1440 * 15 сек = 24 часа).
• Храним среднее из 20 значений, всего 2016 штук (2016 * 15 сек = 7 дней).

Закрепляем навыки. Настройки для кнопки:

# each 60 sec
temps.def=DERIVE,60,0,100,60
# 6h/60s : 5m/24h : 5m/7d
temps.archives=MAX,0.5,1,360:MAX,0.5,5,288:MAX,0.5,30,336
temps.items=Test_button_soc

Здесь я сохраняю значения каждые 60 сек и храню максимальное.

Да. Если в настройке persistence задать сохранение чаще, чем step в конфиге rrd4j, то хранилище будет группировать значения агрегатной функцией из первого архива.

Теперь вернёмся к настройке OpenHab (привожу только часть Items):

Items
{ gTemperature* : strategy = every15sec, restoreOnStartup Test_button_soc : strategy = everyMinute, everyChange
}

Я задал для термометров сохранение каждые 15 сек и задал настройки для кнопки. Мы будет сохранять её каждую минуту и при изменении, что в совокупности с агрегатной функцией MAX позволит записать в архив 1, если кнопка была хоть раз нажата в течение минуты.

Доделываю sitemap и вот что вижу:

OpenHab — хорошее решение для «умного дома» и автоматизации ЖКХ. Пользуйтесь и расширяйте русское сообщество!


x

Ещё Hi-Tech Интересное!

[Перевод] Микросервисы на Go с помощью Go kit: Введение

Эта статья — введение в Go kit. В этой статье я опишу использование Go kit, набора инструментов и библиотек для создания микросервисов на Go. Первая часть в моем блоге, исходный код примеров доступен здесь. Когда вы разрабатываете облачно-ориентированную распределенную систему, ...

Собери свой танк

Мужик собирает танки из кусочков, как лего. Уже собрал 20 штук. («Шейте красное с красным, жёлтое с жёлтым, белое с белым»). Сергей Чибинеев — реаниматолог, он знает как пришить человеку пару оторванных кусочков. В лучших традициях Jagged Alliance и Fallout. ...