|
|
Строка 377: |
Строка 377: |
|
|
|
|
|
=Исправленный и дополненный скрипт и template для Zabbix= |
|
=Исправленный и дополненный скрипт и template для Zabbix= |
|
+ |
==Конфиг для агента== |
− |
|
|
|
* <code>UserParameter-voltage.conf</code> (часть <code>UserParameter=voltage[*] ... </code> не используется ) |
|
* <code>UserParameter-voltage.conf</code> (часть <code>UserParameter=voltage[*] ... </code> не используется ) |
|
<PRE> |
|
<PRE> |
Строка 383: |
Строка 383: |
|
UserParameter=voltage.discovery,/usr/bin/cat /etc/zabbix/scripts/voltages.json | tee -a /var/log/zabbix-agent-debug.log |
|
UserParameter=voltage.discovery,/usr/bin/cat /etc/zabbix/scripts/voltages.json | tee -a /var/log/zabbix-agent-debug.log |
|
</PRE> |
|
</PRE> |
|
+ |
==systemd unit== |
|
+ |
<PRE> |
|
+ |
[Unit] |
|
+ |
Description=Zabbix Agent Voltmeter |
|
+ |
After=syslog.target |
|
+ |
After=network.target |
|
|
|
|
|
+ |
[Service] |
|
+ |
Type=simple |
|
+ |
Restart=on-failure |
|
+ |
ExecStart=/etc/zabbix/scripts/voltage_new.py |
|
+ |
[Install] |
|
+ |
WantedBy=multi-user.target |
|
+ |
</PRE> |
|
==Скрипт== |
|
==Скрипт== |
|
{{#spoiler:show=voltage_new.py| |
|
{{#spoiler:show=voltage_new.py| |
Мониторинг напряжения на UPS Luxeon
Прежде чем начать
- В примере в схеме общий минус так как сама плата Raspberry питается от тех же батарей. Если это не так, и плата питается от 220 через блок питания, то нужно соединить "землю" платы и минус батарейного блока!
- К датчику напряжения подключается ОДИН провод, если подключить два все сгорит, не нужно так! Вторая точка, относительно которой производится измерение это земля и именно для этого соединяется земля платы и минус батарейной сборки
Постановка задачи
Есть UPS/инвертор (24В): Файл:Ep3000-pro (1).pdf
К нему подключены 2 батареи по 100 Ач, 12Вб последовательно. RS232 или не работает или использует протокол с которым я не смог разобраться.
Для прогнозирования времени работы единственный способ получить данные - это мониторинг напряжения на аккумуляторах.
Вся схема собрана "на коленке" за час, из тех деталей что были в наличии на макетной плате.
Внешний вид, датчики закреплены канцелярскими булавками
Макетная плата, оранжевый - +5в, желтый - земля, Коричневый - SCL, Красный - SDA
Аппаратная часть
Для мониторинга использую то что есть под рукой а именно
- Raspberry Pi Model 1 (самая старая, какая есть)
- Датчики INA-219 (2шт используются, еще 2 на случай подключения большего числа батарей)
INA-219 подключается по интерфейсу I²C (2 проводной интерфейс), питание на датчик подается с распберри
У распберри
- pin 02 - +5В на Vcc датчиков
- pin 06 - Земля на Gnd датчиков
- pin 03 - I2C SDA (данные) на SDA датчиков
- pin 05 - I2C SCL (синхронизация) на SCL датчиков
- Напряжение которое требуется измерять подключается на
+Vin
, измерение происходит относительно уровня земли
- Датчики и RaspberryPi питаются от тех же 24В через DC-DC преобразователь с выходом USB (преобразователь подключен на клеммы батарей UPSa).
Всего на одну шину можно подключить 4 датчика, адрес на шине задается с помошью перемычек (2 перемычки, дают 4 возможных адреса)
Другими словами все датчики подключены к 4 пинам распберри, отдельные пины для каждого датчика не нужны
Программная часть
Требуется включение поддержки I2C со стороны линукса
Самый простой способ - использовать raspi-config
который пропишет все что надо в /etc/modules-load.d/modules.conf
или вручную загрузить нужные модули:
lsmod | grep i2c
i2c_bcm2835 16384 0
i2c_dev 20480 0
После загрузки модулей можно просканировать шину на предмет устройств:
i2cdetect 1
Параметр 1
это номер шины I2C ( их может быть более чем одна, но в моем случае - одна, с номером 1, /dev/i2c-1
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-1.
I will probe address range 0x03-0x77.
Continue? [Y/n] y
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: 40 41 -- -- 44 45 -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
В этом примере видно что есть устройства на адресах 0x40
, 0x41
, 0x44
, 0x45
(У меня фактически включены 4 датчика, но 2 не используются)
Для снятия данных с датчика использую простой скрипт на Python
,
библиотека для работы с ina-219 может быть установлена командой pip3 install pi-ina219
#!/usr/bin/env python3
from ina219 import INA219
from ina219 import DeviceRangeError
import time
SHUNT_OHMS = 0.1
COUNT = 10
def read():
V = ina1.voltage()
return V
ina1 = INA219(SHUNT_OHMS,address=0x40)
ina1.configure()
if __name__ == "__main__":
c = 0
voltage_summ = 0
while c < COUNT:
try:
current_voltage = read()
if current_voltage < 18:
raise ValueError("Voltage can't be < 18v");
voltage_summ = voltage_summ + current_voltage
c = c + 1
time.sleep(0.01)
except:
time.sleep(0.2)
pass
print("{V1:.3f}".format(V1=voltage_summ/COUNT))
В этом скрипте
ina1 = INA219(SHUNT_OHMS,address=0x40)
- 0x40 это адрес датчика, для снятия данных с нескольких датчиков его можно вынести в параметры или сделать копию скрипта =)
if current_voltage < 18: raise ValueError("Voltage can't be < 18v");
нужна для того что бы игнорировать значения ниже 18Вольт, так как иногда происходит ошибка измерения
COUNT = 10
число измерений, результат берется как среднее значение
Интеграция с системой мониторинга Zabbix
Для того что бы избежать возможных проблем данные снимаются регулярно, раз в минуту используя cron
:
cat /etc/cron.d/voltage
(за одно не нужно давать sudo
пользователю zabbix)
* * * * * root /etc/zabbix/scripts/voltage_0x40.py > /etc/zabbix/scripts/voltage_24v
* * * * * root /etc/zabbix/scripts/voltage_0x41.py > /etc/zabbix/scripts/voltage_12v
Данные по 2 точкам (12В и 24В) сохраняются в тестовые файлы, например /etc/zabbix/scripts/voltage_24v
Zabbix-agent
прочто читает эти файлы и отдает как значения ключей
cat /etc/zabbix/zabbix_agentd.conf.d/UserParameter-voltage.conf
UserParameter=ups.luxeon.voltage_24v_level,/usr/bin/cat /etc/zabbix/scripts/voltage_24v
UserParameter=ups.luxeon.voltage_12v_level,/usr/bin/cat /etc/zabbix/scripts/voltage_12v
Проверить работу можно командой zabbix_get
zabbix_get -s 192.168.29.29 -k ups.luxeon.voltage_12v_level
13.698
-s 192.168.29.29
- адрес заббикс-агента, IP адрес RaspberryPi куда подключены датчики
-k ups.luxeon.voltage_12v_level
имя ключа
Zabbix Template
Темплейт (очень упрощенный)
<?xml version="1.0" encoding="UTF-8"?>
<zabbix_export>
<version>5.0</version>
<date>2022-12-29T08:09:19Z</date>
<groups>
<group>
<name>Luxeon</name>
</group>
</groups>
<templates>
<template>
<template>Temlate Luxeon Voltage</template>
<name>Temlate Luxeon Voltage</name>
<groups>
<group>
<name>Luxeon</name>
</group>
</groups>
<applications>
<application>
<name>Luxeon</name>
</application>
</applications>
<items>
<item>
<name>ups.luxeon.voltage_12v_level</name>
<key>ups.luxeon.voltage_12v_level</key>
<value_type>FLOAT</value_type>
<units>V</units>
<applications>
<application>
<name>Luxeon</name>
</application>
</applications>
<triggers>
<trigger>
<expression>{max(5)}<11.5</expression>
<name>ups.luxeon.voltage_12v_level is critical low ( <11.5v during last 5 minutes)</name>
<priority>HIGH</priority>
</trigger>
<trigger>
<expression>{max(5)}<12</expression>
<name>ups.luxeon.voltage_12v_level is low ( <12v during last 5 minutes)</name>
<priority>AVERAGE</priority>
</trigger>
<trigger>
<expression>{nodata(15m)}=1</expression>
<name>ups.luxeon.voltage_12v_level NODATA</name>
<priority>HIGH</priority>
</trigger>
<trigger>
<expression>{max(5)}<24</expression>
<name>ups.luxeon.voltage_24v_level is low ( <24v during last 5 minutes)</name>
<priority>AVERAGE</priority>
</trigger>
</triggers>
</item>
<item>
<name>ups.luxeon.voltage_12v_level_5min_avg</name>
<type>CALCULATED</type>
<key>ups.luxeon.voltage_12v_level_5min_avg</key>
<value_type>FLOAT</value_type>
<params>avg(ups.luxeon.voltage_12v_level, 5m)</params>
<applications>
<application>
<name>Luxeon</name>
</application>
</applications>
</item>
<item>
<name>ups.luxeon.voltage_24v_level</name>
<key>ups.luxeon.voltage_24v_level</key>
<value_type>FLOAT</value_type>
<units>V</units>
<applications>
<application>
<name>Luxeon</name>
</application>
</applications>
<triggers>
<trigger>
<expression>{max(5)}<23</expression>
<name>ups.luxeon.voltage_24v_level is critical low( <23v during last 5 minutes)</name>
<priority>HIGH</priority>
</trigger>
<trigger>
<expression>{nodata(15m)}=1</expression>
<name>ups.luxeon.voltage_24v_level NODATA</name>
<priority>HIGH</priority>
</trigger>
</triggers>
</item>
<item>
<name>ups.luxeon.voltage_24v_level_5min_avg</name>
<type>CALCULATED</type>
<key>ups.luxeon.voltage_24v_level_5min_avg</key>
<value_type>FLOAT</value_type>
<params>avg(ups.luxeon.voltage_24v_level, 5m)</params>
<applications>
<application>
<name>Luxeon</name>
</application>
</applications>
</item>
</items>
<screens>
<screen>
<name>Voltage</name>
<hsize>1</hsize>
<vsize>2</vsize>
<screen_items>
<screen_item>
<resourcetype>0</resourcetype>
<style>0</style>
<resource>
<name>UPS Voltage (5min avg)</name>
<host>Temlate Luxeon Voltage</host>
</resource>
<width>1500</width>
<height>300</height>
<x>0</x>
<y>0</y>
<colspan>1</colspan>
<rowspan>1</rowspan>
<elements>0</elements>
<valign>0</valign>
<halign>0</halign>
<dynamic>0</dynamic>
<sort_triggers>0</sort_triggers>
<url/>
<application/>
<max_columns>3</max_columns>
</screen_item>
<screen_item>
<resourcetype>0</resourcetype>
<style>0</style>
<resource>
<name>UPS Voltage</name>
<host>Temlate Luxeon Voltage</host>
</resource>
<width>1500</width>
<height>300</height>
<x>0</x>
<y>1</y>
<colspan>1</colspan>
<rowspan>1</rowspan>
<elements>0</elements>
<valign>0</valign>
<halign>0</halign>
<dynamic>0</dynamic>
<sort_triggers>0</sort_triggers>
<url/>
<application/>
<max_columns>3</max_columns>
</screen_item>
</screen_items>
</screen>
</screens>
</template>
</templates>
<graphs>
<graph>
<name>UPS Voltage</name>
<yaxismin>9</yaxismin>
<yaxismax>30</yaxismax>
<ymin_type_1>FIXED</ymin_type_1>
<ymax_type_1>FIXED</ymax_type_1>
<graph_items>
<graph_item>
<sortorder>1</sortorder>
<color>1A7C11</color>
<item>
<host>Temlate Luxeon Voltage</host>
<key>ups.luxeon.voltage_12v_level</key>
</item>
</graph_item>
<graph_item>
<sortorder>2</sortorder>
<color>F63100</color>
<item>
<host>Temlate Luxeon Voltage</host>
<key>ups.luxeon.voltage_24v_level</key>
</item>
</graph_item>
</graph_items>
</graph>
<graph>
<name>UPS Voltage (5min avg)</name>
<yaxismin>9</yaxismin>
<yaxismax>30</yaxismax>
<ymin_type_1>FIXED</ymin_type_1>
<ymax_type_1>FIXED</ymax_type_1>
<graph_items>
<graph_item>
<sortorder>1</sortorder>
<color>1A7C11</color>
<item>
<host>Temlate Luxeon Voltage</host>
<key>ups.luxeon.voltage_12v_level_5min_avg</key>
</item>
</graph_item>
<graph_item>
<sortorder>2</sortorder>
<color>F63100</color>
<item>
<host>Temlate Luxeon Voltage</host>
<key>ups.luxeon.voltage_24v_level_5min_avg</key>
</item>
</graph_item>
</graph_items>
</graph>
</graphs>
</zabbix_export>
Исправленный и дополненный скрипт и template для Zabbix
Конфиг для агента
UserParameter-voltage.conf
(часть UserParameter=voltage[*] ...
не используется )
UserParameter=voltage[*],/usr/bin/cat /etc/zabbix/scripts/voltages.json | /usr/bin/cat /etc/zabbix/scripts/voltages.json | /usr/bin/jq --arg jq_var $1 '.data | .[] | select(."{#VOLTMETER_NAME}" == $jq_var) | ."{#VOLTAGE}"' 2>&1 | tee -a /var/log/zabbix-agent-debug.log
UserParameter=voltage.discovery,/usr/bin/cat /etc/zabbix/scripts/voltages.json | tee -a /var/log/zabbix-agent-debug.log
systemd unit
[Unit]
Description=Zabbix Agent Voltmeter
After=syslog.target
After=network.target
[Service]
Type=simple
Restart=on-failure
ExecStart=/etc/zabbix/scripts/voltage_new.py
[Install]
WantedBy=multi-user.target
Скрипт
#!/usr/bin/env python3
import json
from ina219 import INA219
from ina219 import DeviceRangeError
import time
#Имена вольтметров и их адреса на шине
VOLTMETERS = {
"12V": 0x41,
"24V": 0x40
}
# Переменная для сохранения данных
VOLTAGES = []
SHUNT_OHMS = 0.1
# число чтений которое делается с датчика, результат усредняется
READS = 10
# Если до того как произошло READS успешных чтений
# число ошибок привысит это значение то чтение на этой
# иттерации будет прервано
MAX_READ_ERRORS = 20
# Минимальное и максимальное напряжение которое считается нормой
# что бы ошибки чтения не оказывали влияния на результат
MIN_VALID_VOLTAGE = {
"12V": 9,
"24V": 18
}
#
MAX_VALID_VOLTAGE = {
"12V": 18,
"24V": 36
}
# Пауза после успешного чтения
PAUSE_SUCCESS = 0.1
# Пауза после чтения с ошибкой
PAUSE_ERROR = 0.5
# Пауза между циклами
PAUSE_CYCLE = 20
#PAUSE_CYCLE = 0
# Файл для сохранения результатов, обновляется при каждом чтении
RESULT_FILE="/etc/zabbix/scripts/voltages.json"
# Эта структура описывает преобразования которые нужно сделать с сырыми
# данными после того как они получены
# Поля имеют следующий смысл
# 24V - имя сенсора для которо нужно делать преобразование
# operation - тип операции, например отнять какое-то значение или прибавить
# operand - значение которое нужно отнять или прибавить или на которое домножить
# operand_type - тип этого значения, например sensor - это значение из другого сенсора
# или const - константа
# Стркутра
# {
# "operation": "-",
# "operand": "12V"
# "operand_type": "sensor"
# }
# означает что для того что бы получить правильный результат измерений требуется от полученного
# значения сенсора отнять значение сенсора с именем "12V"
#
# Вторая структура означает умножение на константу 1 и ничего по сути не делает - оставлена для примера
POSTPROCESSING = {
"24V": [
{
"operation": "-",
"operand": "12V",
"operand_type": "sensor"
},
{
"operation": "*",
"operand": 1,
"operand_type": "const"
}
]
}
def get_operand_value(operand, operand_type, inventory):
print("operand: {}".format(operand))
print("inventory: {}".format(inventory))
if operand_type == "const":
return operand
elif operand_type == "sensor":
try:
return inventory[operand]["VOLTAGE_PROCESSED"]
except KeyError:
return inventory[operand]["VOLTAGE"]
else:
raise NotImplementedError()
# Базовый класс от которого наследуются операции
class _BaseAction:
def __init__(self, operand, **kwargs):
print("_BaseAction: __init__ operand: {}, kwargs : {}".format(operand, kwargs))
self.operand = operand
self._kwargs = kwargs
self.operand_type = kwargs['operand_type']
def __call__(self, target, inventory):
print("_BaseAction: __call__")
raise NotImplementedError()
# Тут только 2 класса (пока) - вычитание и умножение, так как пока других дейтвий не требуется
# Вычитание
class SubstractAction(_BaseAction):
# target - это то значение для которого производятся вычисления и модификации
# inventory - список других значений сенсоров которые могут потребоваться при вычилении
def __call__(self, target, inventory):
print("SubstractAction: __call__ \ntarget: {}\ninventory: {}".format(target, inventory))
operand_value = get_operand_value(self.operand, self.operand_type, inventory)
print("Operand is: {}, Actual operand value: {}".format(self.operand, operand_value))
# так как шагов обработки может быть несколько то используем уже обработанные данные на кажом шаге
# за исключением шага когда таких данных еще нет
try:
actual_current_value = target["VOLTAGE_PROCESSED"]
except KeyError:
actual_current_value = target['VOLTAGE']
return actual_current_value - operand_value
# Умножение
class MultipleAction(_BaseAction):
def __call__(self, target, inventory):
print("MultipleAction: __call__")
operand_value = get_operand_value(self.operand, self.operand_type, inventory)
print("Operand is: {}, Actual operand value: {}".format(self.operand, operand_value))
try:
actual_current_value = target["VOLTAGE_PROCESSED"]
except KeyError:
actual_current_value = target['VOLTAGE']
return actual_current_value * operand_value
# Метод который вызыввается
def produce_action(operation):
print("Getting produce_action for {}".format(operation))
action_map = {
'-': SubstractAction,
'*': MultipleAction
}
# Это копирует параметр operation в args
# для того что бы его можно было безопастно модифицировать
args = dict(operation)
# Получить имя операции из аргументов
operation_name = args.pop('operation')
try:
action_class = action_map[operation_name]
print("Action class: {}".format(action_class))
except KeyError:
raise ValueError(f'Unknown operation reference {operation_name!r}')
operand = args.pop('operand')
print("Creating new instance of CLASS {} with parameters: operand: {}, args {}".format(action_class, operand, args))
action_class_instance = action_class(operand, **args)
# возвращает экземпляр класса который "знает" про то с каким операндом ему предстоит работать
# и то какого типа этот операнд
return action_class_instance
# Короткий метод для чтения
def read(ina):
V = ina.voltage()
return V
def read_with_retry(voltmeter, reads, max_read_errors):
# Эта функция пробует несколько раз прочитать данные,
# отбраывая те что считает не валидными и делает несколько попыток при ошибках
current_read_number = 0
voltage_summ = 0
read_errors = 0
print("Reading voltmeter: {} address: {}".format(voltmeter, str(VOLTMETERS[VOLTMETER])))
while current_read_number < reads:
# иногда все таки происходят ошибки чтения и
# заведомо неверные значения исключаем
try:
# каждый раз заново делается инициализация так как адреса разные
ina = INA219(SHUNT_OHMS,address=VOLTMETERS[VOLTMETER])
ina.configure()
current_voltage = read(ina)
if current_voltage < MIN_VALID_VOLTAGE[VOLTMETER]:
# увеличиваем счетчик ошибок, это нужно для того,
# что бы не попасть в вечный цикл если один из вольтметров недосупен
raise ValueError("Not expected data: Got Voltage {} can't be < expected minimum {}v".format(
current_voltage, MIN_VALID_VOLTAGE[VOLTMETER]));
elif current_voltage > MAX_VALID_VOLTAGE[VOLTMETER]:
raise ValueError("Not expected data: Got Voltage {} can't be > expected maximum {}v".format(
current_voltage, MAX_VALID_VOLTAGE[VOLTMETER]));
else:
# Учитываем только данные полученные в успешных попытках
voltage_summ = voltage_summ + current_voltage
current_read_number = current_read_number + 1
time.sleep(PAUSE_SUCCESS)
except Exception as E:
# при получении ошибок увеличиваем счетчик что бы не получить вечный цикл и выводим сообщение о том какая ошибка
read_errors = read_errors + 1
print("Error reading voltmeter {}, Error number {}/{}: {}".format(voltmeter, read_errors, max_read_errors, E))
time.sleep(PAUSE_ERROR)
pass
# выйти из цикла чтений если число ошибок превысило максимальный
if (read_errors >= max_read_errors):
break
return voltage_summ/reads
if __name__ == "__main__":
while True:
VOLTAGES = []
for VOLTMETER in VOLTMETERS.keys():
current_voltage = read_with_retry(VOLTMETER, READS, MAX_READ_ERRORS)
VOLTAGES.append(
{
"VOLTMETER_NAME": VOLTMETER,
"VOLTAGE": current_voltage
}
)
print(json.dumps(VOLTAGES, sort_keys=True, indent=4))
# Это просто удобная переменная что бы можно было удобно
# получать данные для вычислений по сути не больше чем промежуточная переменная
inventory = {}
for m in VOLTAGES:
inventory[m['VOLTMETER_NAME']] = m
print("Inventory: {} ".format(json.dumps(inventory, sort_keys=True, indent=4)))
VOLTAGES_PROCESSED = []
# Пройти по всем считанным вольтметрам
for entry in VOLTAGES:
print("\n\nStaring Processing entry: {}".format(entry))
name = entry['VOLTMETER_NAME']
entry['VOLTAGE_PROCESSED'] = entry['VOLTAGE']
subject = {}
try:
subject = inventory[name]
except KeyError:
print(f'Error - inventory {name} entry missing')
continue
print("Subject: {}".format(subject))
try:
# leverage_stream - последовательнсть операций которые нужно сделать
leverage_stream = POSTPROCESSING[name]
print("leverage_stream = {}".format(json.dumps(leverage_stream, indent=4)))
except KeyError:
print("leverage_stream is not defined, so no actions needed")
print("Finishing Processing entry {}".format(entry))
VOLTAGES_PROCESSED.append(entry)
continue
for leverage in leverage_stream:
print(f'\n\nStarting leverag {leverage} for entry {entry}\n\n')
action = produce_action(leverage)
print("Calling metod __call__ for class {}".format(action))
processed_data = action(entry, inventory)
print("Processed data is: {}".format(processed_data))
if processed_data is None:
processed_data = entry['VOLTAGE']
entry['VOLTAGE_PROCESSED'] = processed_data
print(f'\n\nFinishing leverag {leverage} for entry {entry}\n\n')
VOLTAGES_PROCESSED.append(entry)
print("Finishing Processing entry {}".format(entry))
#time.sleep(PAUSE_CYCLE)
print(json.dumps(VOLTAGES_PROCESSED, indent=4))
with open(RESULT_FILE, "w") as result_file:
result_file.write(json.dumps(VOLTAGES_PROCESSED, sort_keys=True, indent=4))
print("Sleeping for {} before next cycle".format(PAUSE_CYCLE))
time.sleep(PAUSE_CYCLE)
Темплейт