Мысль — это инструмент, с помощью которого человек создает выбор.
Айн Рэнд
— Нам нужно реализовать систему мониторинга и оповещения энергопотребления нашего ЦОД-а по каждой входной фазе, со всеми плюшками — звонками или смс-ками ответственным людям и историей событий.
— Какой бюджет?
— Как всегда — чем меньше, тем лучше.
— У нас есть *VendorName*, нужно лишь докупить для него датчиков. Но стоимость каждой опции начинается от 100$ и платформа закрытая.
— А может быть попробуем Arduino?
— Что это? Хотя… Сколько времени нужно на реализацию и сколько это стоит?
Таким был диалог трех инженеров одним рабочим днем.
Я не программист и не специалист по микроэлектронике, но меня всегда привлекали новые технологии, какими бы они ни были.
Осторожно! Очень много картинок.
— Что делать?
Что делать?
Я же не умею… (вставить любую упущенную возможность в жизни).
— А что ты умеешь?
— Вот… И вот это… И еще немного того.
— Ты родился с этими умениями?
— Нет, меня научили…
Эта инструкция для тех, кто очень хотел бы попробовать, но боится и не знает, с чего начать.
Шаг 1. Купить любую платформу, которая вам пришлась по душе (небольшой совет: не забывайте, что вам жизненно необходим программатор либо USB на борту для заливки ваших скетчей).
Мой выбор пал на Freaduino UNO V1.8 (ATmega 328), потому что в моем родном городе есть всего один магазин, который работает с крупными организациями по безналичному расчету.
Так же мне потребовался Ethernet shield для подключения к сети.
Так же мне повезло найти уже готовый Shield для подключения 3-х датчиков переменного тока с LCD дисплеем и беспроводным интерфейсом.
Ну и сами сенсоры.
Заказать, дождаться курьера, угостить его шоколадкой, нетерпеливо заняться анпакингом.
Шаг 2. Установите Arduino IDE для своей OS, скачав с arduino.cc/en/Main/Software
Шаг 3. Подключаем Arduino к компьютеру и настраиваем софт. Выбираем модель нашей платы и порт подключения (он появляется ПОСЛЕ подключения).
Заливаем скетч в Arduino.
#include <SPI.h>
#include "EmonLib.h"
#include <LCD5110_Graph_SPI.h>
#include <Ethernet.h>
#include <Streaming.h>
#include <Flash.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE }; // MAC адрес устройства проверьте, что такой не существует в вашей сети
byte ip[] = { 10, 10, xx, y1 }; // IP адрес устройства
byte subnet[] = {255, 255, 255, 0}; // Маска сети
byte gateway[] = {10, 10, xx, y2}; // Шлюз по умолчанию
EthernetServer server(80); // Веб сервер
EthernetClient http_client; // Веб клиент
char serverName[] = "10.10.xx.y3"; // Адрес звонилки
LCD5110 myGLCD(5,6,3); // Инициализация LCD
extern uint8_t SmallFont[];
EnergyMonitor emon1;
EnergyMonitor emon2;
EnergyMonitor emon3;
int msec = 0;
int msecP1 = 0;
int msecP2 = 0;
int msecP3 = 0;
int LevelAlarm = 300;
boolean Alarm1 = false; // Начальные значения при включении
boolean Alarm2 = false;
boolean Alarm3 = false;
boolean Start1 = true; // Флаг включения
double Irms1;
double Irms2;
double Irms3;
double Delta1=0;
double Delta2=0;
double Delta3=0;
double Pcur1;
double Pcur2;
double Pcur3;
double Kphase = 0.220; // Если у вас 380 - поменяйте значение
double PAlarm = 0.05; // Критичное значение после которого включаем тревогу
char buf[15];
char tbuf[5];
char buffer[50];
String AMsg="";
String MMsg="";
void setup() {
Serial.begin(9600);
Serial.println("Starting init...");
Ethernet.begin(mac, ip, gateway, subnet);
myGLCD.InitLCD(70);
myGLCD.setFont(SmallFont);
myGLCD.clrScr();
emon1.current(0, 111.1); //инициализация
emon2.current(1, 111.1);
emon3.current(2, 111.1);
myGLCD.clrScr();
myGLCD.print("Energy Monitor", 0, 0);
myGLCD.print(" calibrating", 0, 10);
myGLCD.print("unplug sensors", 0, 20);
//myGLCD.print("12345678901234", 0, 30); 14 символов в строке дисплея
myGLCD.update();
double Irms1 = emon1.calcIrms(1480); // первые значения снятые с сенсоров - ооочень отличаются... не будем их учитывать в дельтах
double Irms2 = emon2.calcIrms(1480);
double Irms3 = emon3.calcIrms(1480);
double cIrms;
#define WINc 20 // количество пробных попыток для калибровки
double Irms[WINc];
cIrms = 0;
for (int i=0; i<WINc; i++) {
Irms[i] = emon1.calcIrms(1480);
cIrms = cIrms + Irms[i];
delay(1);
}
Delta1 = cIrms/WINc;
cIrms = 0;
for (int i=0; i<WINc; i++) {
Irms[i] = emon2.calcIrms(1480);
cIrms = cIrms + Irms[i];
delay(1);
}
Delta2 = cIrms/WINc;
cIrms = 0;
for (int i=0; i<WINc; i++) {
Irms[i] = emon3.calcIrms(1480);
cIrms = cIrms + Irms[i];
delay(1);
}
Delta3 = cIrms/WINc;
Serial.println(Delta1); // выведем значения в консоль
Serial.println(Delta2);
Serial.println(Delta3);
char D11[10]; dtostrf(Delta1, 2, 10, D11);
char D12[10]; dtostrf(Delta2, 2, 10, D12);
char D13[10]; dtostrf(Delta3, 2, 10, D13);
myGLCD.clrScr(); // выведем значения на LCD
myGLCD.print("D1: ", 0, 20);
myGLCD.print(D11, 20, 20);
myGLCD.print("D2: ", 0, 10);
myGLCD.print(D12, 20, 10);
myGLCD.print("D3: ", 0, 0);
myGLCD.print(D13, 20, 0);
myGLCD.print(" wait 5 sec. ", 0, 30);
myGLCD.update();
delay(5000);
myGLCD.clrScr();
myGLCD.update();
}
void loop() { // сам цикл
Irms1 = emon1.calcIrms(1480) - Delta1; Irms1 = abs(Irms1); Pcur1 = Irms1*Kphase;
Irms2 = emon2.calcIrms(1480) - Delta2; Irms2 = abs(Irms2); Pcur2 = Irms2*Kphase;
Irms3 = emon3.calcIrms(1480) - Delta3; Irms3 = abs(Irms3); Pcur3 = Irms3*Kphase;
Serial.println(); // выводим значения в консоль
Serial.print(" I1:"); Serial.print(Irms1); Serial.print(" P1:"); Serial.print(Pcur1); Serial.println();
Serial.print(" I2:"); Serial.print(Irms2); Serial.print(" P2:"); Serial.print(Pcur2); Serial.println();
Serial.print(" I3:"); Serial.print(Irms3); Serial.print(" P3:"); Serial.print(Pcur3); Serial.println();
myGLCD.print("Energy Monitor", 0, 0); // Выводим значения на LCD
myGLCD.update();
dtostrf(Pcur1,4,2,tbuf); sprintf(buf, "P1: %s kVt", tbuf); myGLCD.print(buf, 0, 30);
dtostrf(Pcur2,4,2,tbuf); sprintf(buf, "P2: %s kVt", tbuf); myGLCD.print(buf, 0, 20);
dtostrf(Pcur3,4,2,tbuf); sprintf(buf, "P3: %s kVt", tbuf); myGLCD.print(buf, 0, 10);
myGLCD.print(AMsg,0,40);
myGLCD.update();
EthernetClient client = server.available(); // вдруг кто-то постучался по 80 порту! - формируем и отдаем страничку
if (client) {
Serial.println(">>>>> HTTP clent connect...");
boolean current_line_is_blank = true;
while (client.connected()) {
if (client.available()) {
char c = client.read();
if (c == 'n' && current_line_is_blank) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
client.print("Pcur1="); client.print(Pcur1); client.print("<br>");
client.print("Pcur2="); client.print(Pcur2); client.print("<br>");
client.print("Pcur3="); client.print(Pcur3); client.print("<br>");
client.println();
client.print(" Delta1:"); client.print(Delta1);
client.print(" Delta2:"); client.print(Delta2);
client.print(" Delta3:"); client.print(Delta3);
client.println();
break;
}
if (c == 'n') {
current_line_is_blank = true;
}
else if (c != 'r') {
current_line_is_blank = false;
}
}
}
delay(10);
client.stop();
}
if ( (Start1)&&(msec >= 100) ) { // Проверяем сколько времени прошло с момента включения
Start1 = false;
}
if ( !Start1) {
MMsg = "";
isAlarmP1();
isAlarmP2();
isAlarmP3();
AMsg = "";
AMsg += MMsg;
AMsg += " alarm";
Serial.println(AMsg);
}
else {
itoa(100-msec,buf,10);
//Serial.println(buf);
//Serial.println(" <->");
MMsg = buf;
AMsg = "";
AMsg += MMsg;
AMsg += " ";
//AMsg += " ";
Serial.println(AMsg);
}
if ( msec <= 32766) {
msec++;
}
else {
msec = 0;
}
// again
}
void postPage (char *webPage) { // Аларм, аларм, аларм! Надо дернуть пейджу!
if (http_client.connect(serverName,80)>0) {
http_client.print("GET ");
http_client.print(webPage);
http_client.print(" HTTP/1.0");
http_client.println("User-Agent: Arduino 1.0");
http_client.println();
Serial.println(" >> GET URL");
}
http_client.stop();
http_client.flush();
}
void isAlarmP1 () {
// Сработал первый аларм
if ( (Pcur1 < PAlarm) ) {
if ( Alarm1 != true ) {
postPage("/alarm?q=P1");
msecP1 = msec;
}
else {
if ( abs(msec - msecP1) >= LevelAlarm ) { // дополнительная ветка повтора аларма
Serial.println(" >> Repeat alarm P1");
// postPage("/alarm?q=P1");
msecP1 = msec;
}
}
Alarm1 = true;
MMsg += "P1";
}
else {
Alarm1 = false;
MMsg += " ";
msecP1 = 0;
}
}
void isAlarmP2 () {
// Сработал второй аларм
if ( (Pcur2 < PAlarm) ) {
if ( Alarm2 != true ) {
postPage("/alarm?q=P2");
msecP2 = msec;
}
else {
if ( abs(msec - msecP2) >= LevelAlarm ) { // дополнительная ветка повтора аларма
Serial.println(" >> Repeat alarm P2");
// postPage("/alarm?q=P2");
msecP2 = msec;
}
}
Alarm2 = true;
MMsg += "P2";
}
else {
Alarm2 = false;
MMsg += " ";
msecP2 = 0;
}
}
void isAlarmP3 () {
// Сработал третий аларм
if ( (Pcur3 < PAlarm) ) {
if ( Alarm3 != true ) {
postPage("/alarm?q=P3");
msecP3 = msec;
}
else {
if ( abs(msec - msecP3) >= LevelAlarm ) { // дополнительная ветка повтора аларма
Serial.println(" >> Repeat alarm P3");
// postPage("/alarm?q=P3");
msecP3 = msec;
}
}
Alarm3 = true;
MMsg += "P3";
}
else {
Alarm3 = false;
MMsg += " ";
msecP3 = 0;
}
}
Один из важных вопросов — как это мониторить?
В моей компании используется Zabbix. Давайте его настроим.
Нам нужно выдергивать значения всех трех датчиков с некоторой периодичностью. К счастью, в документации нашлось объяснение как это делать.
Открываем файл настроек /usr/local/etc/zabbix_agentd.conf и создаем пользовательский параметр:
UserParameter=scb.Pcur[*],/usr/local/share/zabbix/externalscripts/get_energy_power.sh "$1" "$2"
UserParameter=(ПридуманноеНазваниеПараметра[входные параметры]), путь до внешнего скрипта $передаваемые параметры.
Теперь внутренности самого скрипта:
#!/bin/bash
if [ ! -f /tmp/html ]
then
/usr/bin/wget -q --proxy=off --connect-timeout=2 --tries=1 -O /tmp/html http://$1/html {} ; 2>/dev/null
fi
/usr/bin/find /tmp -name html -type f -cmin +1 -exec /usr/bin/wget -q --proxy=off --connect-timeout=2 --tries=1 -O /tmp/html http://$1/html {} ; 2>/dev/null
sleep 1
if [ $2 = 1 ];
then
if [ "$(cat /tmp/html | awk -F <br> '{print $1}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r')" = '' ]
then
echo "-1"
else
cat /tmp/html | awk -F <br> '{print $1}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r'
fi
fi
if [ $2 = 2 ];
then
if [ "$(cat /tmp/html | awk -F <br> '{print $2}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r')" = '' ]
then
echo "-1"
else
cat /tmp/html | awk -F <br> '{print $2}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r'
fi
fi
if [ $2 = 3 ];
then
if [ "$(cat /tmp/html | awk -F <br> '{print $3}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r')" = '' ]
then
echo "-1"
else
cat /tmp/html | awk -F <br> '{print $3}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r'
fi
fi
Краткое пояснение:
Проверяем существование файла /tmp/html (это копия нашей странички с результатами измерений), если не существует — дергаем wget-ом с адреса из первого входящего параметра. Если существует, но он старше 1-й минуты — дергаем wget-ом новую версию и сохраняем в /tmp/html. Далее в зависимости от второго входного параметра выбираем, какое из значений вывести на экран. Если по какой-то причине мы не смогли получить данные — выводим -1 (на всякий случай).
Проверяем работу скрипта:
user@host:~/temp> /usr/local/share/zabbix/externalscripts/get_energy_power.sh energy_power 1
0.02
user@host:~/temp> /usr/local/share/zabbix/externalscripts/get_energy_power.sh energy_power 2
0.09
user@host:~/temp> /usr/local/share/zabbix/externalscripts/get_energy_power.sh energy_power 3
0.01
Все готово, проверяем, получает ли zabbix необходимые значения:
user@host:~/temp> zabbix_get -s localhost -k scb.Pcur[energy_power,1]
0.02
user@host:~/temp> zabbix_get -s localhost -k scb.Pcur[energy_power,2]
0.09
user@host:~/temp> zabbix_get -s localhost -k scb.Pcur[energy_power,3]
0.01
Отлично!
Теперь добавим график.
Configurations -> Hosts -> New Host -> Name -> energy_power (DNS имя нашего устройства) -> Save
Создадим Items.
Configurations -> Hosts -> energy_power -> Items -> New Item
Теперь добавим график.
Configurations -> Hosts -> energy_power -> Graphs -> New graph -> добавляем все три параметра, задаем цвета -> Save
Вот и готов график, нужно подождать нексолько минут и появятся данные.
Логика работы устройства:
Подключаем устройство к сети Ethernet (датчики должны «висеть в воздухе», иначе у вас будут огромные дельты).
Подаем питание, оживляя этого Франкенштейна.
Ждем пока на LCD не появятся результаты калибровки — значения дельт.
После этого у вас есть примерно 100 секунд, в течении которых нужно повешать сенсоры на фазы. На LCD в это время выводится обратный отчет как в настоящих боевиках с Брюсом Виллисом.
Когда обратный отчет закончен, устройство переходит в «боевой» режим.
Как только на одном из сенсоров произойдет падение ниже критичной отметки (0.05), устройство веб клиентом дергает URL вида 10.10.xx.y3/alarm?q=PX и она начинает планомерный обзвон всех заинтересованных лиц (к сожалению, не могу рассказать больше, потому что эта звонилка находится вне моей компетенции… я даже не знаю что это за устройство). URL дергается один раз за одну зафиксированную аварию. Если фаза появится, то флаг аварии сбросится и при последующем падении напряжения — URL дернется еще раз. В скетче закоментирована возможность дергать URL повторно через 300 циклов, если авария продолжается.
Дополнительный канал оповещения — СМС с помощью zabbix (на эту тему написано очень много отличных статей и в данной статье не рассматривается).
Для отладки был использован обычный китайский пилот из которого вывели один из проводов и подключили монитор для калибровки.
Итоговый бюджет:
Freaduino UNO V1.8 (ATmega 328) 799 р.
ETHERNET SHIELD V1.0 (WIZNET W5100) 739 р.
ENERGY MONITOR Shield 969 р.
ДАТЧИК ПЕРЕМЕННОГО ТОКА 100А 469 р. (нужны 3 штуки)
Итого: 3914 р.
К сожалению, внедрение затянулось, в нашем городе почему-то не продают SCT-019-000 (датчики на 200А), а эти (SCT-013) просто не подходят по сечению на наши провода… Да и по амперам не проходят. Заказать из Китая не позволяет бухгалтерия. Может быть, есть люди, готовые помочь?
Данная статья посвящается великолепному программисту — Геле Высоцкой.