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

в 20:41, , рубрики: arduino, java, openhab, persistence, rrd, rrd4j, программирование микроконтроллеров, Серверное администрирование

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

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

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

Тут нужно рассказать, что представляет собой это хранилище. Это один или несколько циклических буферов, которые хранят значения параметра с привязкой ко времени. При этом, есть возможность агрегировать несколько изменений в одно значение буфера (например, снимать данные каждую минуту, а хранить одно среднее значение за 5 мин).

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

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

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

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

Для коммуникации с сервером она использует 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. К счастью, он есть в репозитории.

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

Настройка сохранения производится в файле /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 для OpenHab2 persistence - 4

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

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

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

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

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

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

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

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

Вот тут-то и кроется причина того, что нет смысла сохранять значение чаще, чем раз в минуту! В хранилище данные записываются каждые 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 и вот что вижу:

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

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

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

Автор: Борис

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js