Luxeon UPS: различия между версиями

Материал из noname.com.ua
Перейти к навигацииПерейти к поиску
 
(не показаны 34 промежуточные версии этого же участника)
Строка 1: Строка 1:
 
[[Категория:Linux]]
 
[[Категория:Linux]]
  +
[[Категория:UPS]]
  +
[[Категория:I2C]]
  +
[[Категория:RaspberryPi]]
  +
[[Категория:Monitoring]]
  +
[[Категория:INA219]]
  +
[[Категория:Zabbix]]
  +
 
=Мониторинг напряжения на UPS Luxeon=
 
=Мониторинг напряжения на UPS Luxeon=
  +
  +
=Прежде чем начать=
  +
* В примере в схеме <big>'''общий минус'''</big> так как сама плата Raspberry питается от тех же батарей. Если это не так, и плата питается от 220 через блок питания, то нужно соединить "землю" платы и минус батарейного блока!
  +
  +
* К датчику напряжения подключается <big>'''ОДИН провод,'''</big> если подключить два все сгорит, не нужно так! Вторая точка, относительно которой производится измерение это земля и именно для этого соединяется земля платы и минус батарейной сборки
   
 
=Постановка задачи=
 
=Постановка задачи=
Строка 6: Строка 18:
 
К нему подключены 2 батареи по 100 Ач, 12Вб последовательно. RS232 или не работает или использует протокол с которым я не смог разобраться. <BR>
 
К нему подключены 2 батареи по 100 Ач, 12Вб последовательно. RS232 или не работает или использует протокол с которым я не смог разобраться. <BR>
 
Для прогнозирования времени работы единственный способ получить данные - это мониторинг напряжения на аккумуляторах.
 
Для прогнозирования времени работы единственный способ получить данные - это мониторинг напряжения на аккумуляторах.
  +
<BR>
  +
<BR>
  +
Вся схема собрана "на коленке" за час, из тех деталей что были в наличии на макетной плате.
  +
<BR>
  +
  +
  +
[[Файл:Luxeon Voltage 1.JPG|200px|thumb|left|Внешний вид, датчики закреплены канцелярскими булавками]]
  +
[[Файл:Luxeon Voltage 2.jpg|200px|thumb|left|Макетная плата, оранжевый - +5в, желтый - земля, Коричневый - SCL, Красный - SDA]]
  +
[[Файл:Luxeon Voltage 3.jpg|200px|thumb|left|RaspberryPi 1, 2011 года]]
   
 
=Аппаратная часть=
 
=Аппаратная часть=
 
Для мониторинга использую то что есть под рукой а именно
 
Для мониторинга использую то что есть под рукой а именно
 
* Raspberry Pi Model 1 (самая старая, какая есть)
 
* Raspberry Pi Model 1 (самая старая, какая есть)
* Датчики INA-219 (2шт) <BR>
+
* Датчики INA-219 (2шт используются, еще 2 на случай подключения большего числа батарей) <BR>
 
[[Файл:ina219.jpg |200px|ina-219]]<br>
 
[[Файл:ina219.jpg |200px|ina-219]]<br>
 
<BR>
 
<BR>
 
INA-219 подключается по интерфейсу [https://ru.wikipedia.org/wiki/I²C I²C] (2 проводной интерфейс), питание на датчик подается с распберри
 
INA-219 подключается по интерфейсу [https://ru.wikipedia.org/wiki/I²C I²C] (2 проводной интерфейс), питание на датчик подается с распберри
  +
<BR>
  +
<BR>
 
У распберри
 
У распберри
 
* pin 02 - +5В на Vcc датчиков
 
* pin 02 - +5В на Vcc датчиков
* pin 05 - Земля на Gnd датчиков
+
* pin 06 - Земля на Gnd датчиков
 
* pin 03 - I<sup>2</sup>C SDA (данные) на SDA датчиков
 
* pin 03 - I<sup>2</sup>C SDA (данные) на SDA датчиков
 
* pin 05 - I<sup>2</sup>C SCL (синхронизация) на SCL датчиков
 
* pin 05 - I<sup>2</sup>C SCL (синхронизация) на SCL датчиков
  +
* Напряжение которое требуется измерять подключается на <code>'''+Vin'''</code>, измерение происходит относительно уровня земли
  +
* Датчики и RaspberryPi питаются от тех же 24В через DC-DC преобразователь с выходом USB (преобразователь подключен на клеммы батарей UPSa).
 
<BR>
 
<BR>
 
Всего на одну шину можно подключить 4 датчика, адрес на шине задается с помошью перемычек (2 перемычки, дают 4 возможных адреса)
 
Всего на одну шину можно подключить 4 датчика, адрес на шине задается с помошью перемычек (2 перемычки, дают 4 возможных адреса)
Строка 102: Строка 127:
 
* <code>ina1 = INA219(SHUNT_OHMS,address=0x40)</code> - 0x40 это адрес датчика, для снятия данных с нескольких датчиков его можно вынести в параметры или сделать копию скрипта =)
 
* <code>ina1 = INA219(SHUNT_OHMS,address=0x40)</code> - 0x40 это адрес датчика, для снятия данных с нескольких датчиков его можно вынести в параметры или сделать копию скрипта =)
 
* <code>if current_voltage < 18: raise ValueError("Voltage can't be < 18v");</code> нужна для того что бы игнорировать значения ниже 18Вольт, так как иногда происходит ошибка измерения
 
* <code>if current_voltage < 18: raise ValueError("Voltage can't be < 18v");</code> нужна для того что бы игнорировать значения ниже 18Вольт, так как иногда происходит ошибка измерения
  +
* <code>COUNT = 10</code> число измерений, результат берется как среднее значение
  +
  +
  +
=Интеграция с системой мониторинга Zabbix=
  +
Для того что бы избежать возможных проблем данные снимаются регулярно, раз в минуту используя <code>cron</code>:
  +
<code>cat /etc/cron.d/voltage</code><BR>
  +
(за одно не нужно давать <code>sudo</code> пользователю zabbix)
  +
<PRE>
  +
* * * * * root /etc/zabbix/scripts/voltage_0x40.py > /etc/zabbix/scripts/voltage_24v
  +
* * * * * root /etc/zabbix/scripts/voltage_0x41.py > /etc/zabbix/scripts/voltage_12v
  +
</PRE>
  +
Данные по 2 точкам (12В и 24В) сохраняются в тестовые файлы, например <code>/etc/zabbix/scripts/voltage_24v</code>
  +
  +
<code>Zabbix-agent</code> прочто читает эти файлы и отдает как значения ключей
  +
<PRE>
  +
cat /etc/zabbix/zabbix_agentd.conf.d/UserParameter-voltage.conf
  +
</PRE>
  +
<PRE>
  +
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
  +
</PRE>
  +
Проверить работу можно командой <code>zabbix_get</code>
  +
<PRE>
  +
zabbix_get -s 192.168.29.29 -k ups.luxeon.voltage_12v_level
  +
13.698
  +
</PRE>
  +
* <code>-s 192.168.29.29 </code> - адрес заббикс-агента, IP адрес RaspberryPi куда подключены датчики
  +
* <code>-k ups.luxeon.voltage_12v_level</code> имя ключа
  +
  +
=Zabbix Template=
  +
Темплейт (очень упрощенный)
  +
{{#spoiler:show=Temlate Luxeon Voltage|
  +
<PRE>
  +
<?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)}&lt;11.5</expression>
  +
<name>ups.luxeon.voltage_12v_level is critical low ( &lt;11.5v during last 5 minutes)</name>
  +
<priority>HIGH</priority>
  +
</trigger>
  +
<trigger>
  +
<expression>{max(5)}&lt;12</expression>
  +
<name>ups.luxeon.voltage_12v_level is low ( &lt;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)}&lt;24</expression>
  +
<name>ups.luxeon.voltage_24v_level is low ( &lt;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)}&lt;23</expression>
  +
<name>ups.luxeon.voltage_24v_level is critical low( &lt;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>
  +
</PRE>
  +
}}
  +
  +
=Исправленный и дополненный скрипт и template для Zabbix=
  +
==Конфиг для агента==
  +
* не забыть или убрать логгирование или настроить логротейт
  +
* <code>UserParameter-voltage.conf</code> (часть <code>UserParameter=voltage[*] ... </code> не используется )
  +
<PRE>
  +
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
  +
</PRE>
  +
  +
==systemd unit==
  +
<PRE>zabbix-agent-voltmeter.service</PRE>
  +
<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|
  +
<PRE>
  +
#!/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)
  +
  +
  +
</PRE>
  +
}}
  +
  +
==Темплейт==
  +
{{#spoiler:show=Temlate Luxeon Voltage v2|
  +
  +
<PRE>
  +
<?xml version="1.0" encoding="UTF-8"?>
  +
<zabbix_export>
  +
<version>5.0</version>
  +
<date>2024-08-01T08:00:13Z</date>
  +
<groups>
  +
<group>
  +
<name>Power</name>
  +
</group>
  +
</groups>
  +
<templates>
  +
<template>
  +
<template>Template Voltage Sensor</template>
  +
<name>Template Voltage Sensor</name>
  +
<groups>
  +
<group>
  +
<name>Power</name>
  +
</group>
  +
</groups>
  +
<applications>
  +
<application>
  +
<name>Power</name>
  +
</application>
  +
</applications>
  +
<items>
  +
<item>
  +
<name>Voltage discovery</name>
  +
<key>voltage.discovery</key>
  +
<delay>30s</delay>
  +
<history>1h</history>
  +
<trends>0</trends>
  +
<value_type>TEXT</value_type>
  +
<applications>
  +
<application>
  +
<name>Power</name>
  +
</application>
  +
</applications>
  +
<preprocessing>
  +
<step>
  +
<type>JAVASCRIPT</type>
  +
<params>var records = JSON.parse(value);
  +
Zabbix.Log(1, &quot;Item voltage.discovery&quot;)
  +
Zabbix.Log(1, JSON.stringify(records))
  +
Zabbix.Log(1, &quot;END of Item voltage.discovery&quot;)
  +
return value</params>
  +
</step>
  +
</preprocessing>
  +
</item>
  +
</items>
  +
<discovery_rules>
  +
<discovery_rule>
  +
<name>Voltemter Discovery</name>
  +
<type>DEPENDENT</type>
  +
<key>voltmeter.discovery.data</key>
  +
<delay>0</delay>
  +
<item_prototypes>
  +
<item_prototype>
  +
<name>Voltage [{#VOLTMETER_NAME}]</name>
  +
<type>DEPENDENT</type>
  +
<key>voltage.processed[{#VOLTMETER_NAME}]</key>
  +
<delay>0</delay>
  +
<history>1024d</history>
  +
<trends>0</trends>
  +
<value_type>FLOAT</value_type>
  +
<units>V</units>
  +
<applications>
  +
<application>
  +
<name>Power</name>
  +
</application>
  +
</applications>
  +
<preprocessing>
  +
<step>
  +
<type>JSONPATH</type>
  +
<params>$[?(@.VOLTMETER_NAME == &quot;{#VOLTMETER_NAME}&quot;)].VOLTAGE_PROCESSED.first()</params>
  +
</step>
  +
</preprocessing>
  +
<master_item>
  +
<key>voltage.discovery</key>
  +
</master_item>
  +
</item_prototype>
  +
<item_prototype>
  +
<name>Voltage Raw Data [{#VOLTMETER_NAME}]</name>
  +
<type>DEPENDENT</type>
  +
<key>voltage.raw[{#VOLTMETER_NAME}]</key>
  +
<delay>0</delay>
  +
<history>1024d</history>
  +
<trends>0</trends>
  +
<value_type>FLOAT</value_type>
  +
<units>V</units>
  +
<description>Data as-is, without modification</description>
  +
<applications>
  +
<application>
  +
<name>Power</name>
  +
</application>
  +
</applications>
  +
<preprocessing>
  +
<step>
  +
<type>JSONPATH</type>
  +
<params>$[?(@.VOLTMETER_NAME == &quot;{#VOLTMETER_NAME}&quot;)].VOLTAGE.first()</params>
  +
</step>
  +
</preprocessing>
  +
<master_item>
  +
<key>voltage.discovery</key>
  +
</master_item>
  +
</item_prototype>
  +
</item_prototypes>
  +
<graph_prototypes>
  +
<graph_prototype>
  +
<name>Voltage</name>
  +
<graph_items>
  +
<graph_item>
  +
<sortorder>1</sortorder>
  +
<color>1A7C11</color>
  +
<item>
  +
<host>Template Voltage Sensor</host>
  +
<key>voltage.processed[{#VOLTMETER_NAME}]</key>
  +
</item>
  +
</graph_item>
  +
</graph_items>
  +
</graph_prototype>
  +
<graph_prototype>
  +
<name>Voltage (raw data)</name>
  +
<graph_items>
  +
<graph_item>
  +
<sortorder>1</sortorder>
  +
<color>1A7C11</color>
  +
<item>
  +
<host>Template Voltage Sensor</host>
  +
<key>voltage.raw[{#VOLTMETER_NAME}]</key>
  +
</item>
  +
</graph_item>
  +
</graph_items>
  +
</graph_prototype>
  +
</graph_prototypes>
  +
<master_item>
  +
<key>voltage.discovery</key>
  +
</master_item>
  +
<lld_macro_paths>
  +
<lld_macro_path>
  +
<lld_macro>{#VOLTMETER_NAME}</lld_macro>
  +
<path>$.VOLTMETER_NAME</path>
  +
</lld_macro_path>
  +
</lld_macro_paths>
  +
<preprocessing>
  +
<step>
  +
<type>JAVASCRIPT</type>
  +
<params>var records = JSON.parse(value);
  +
Zabbix.Log(1, &quot;Discovery Rule&quot;)
  +
Zabbix.Log(1, JSON.stringify(records))
  +
Zabbix.Log(1, &quot;END OF Discovery Rule&quot;)
  +
return value</params>
  +
</step>
  +
</preprocessing>
  +
</discovery_rule>
  +
</discovery_rules>
  +
<tags>
  +
<tag>
  +
<tag>Application</tag>
  +
<value>Power</value>
  +
</tag>
  +
</tags>
  +
</template>
  +
</templates>
  +
</zabbix_export>
  +
  +
</PRE>
  +
}}

Текущая версия на 16:24, 13 августа 2024


Мониторинг напряжения на UPS Luxeon

Прежде чем начать

  • В примере в схеме общий минус так как сама плата Raspberry питается от тех же батарей. Если это не так, и плата питается от 220 через блок питания, то нужно соединить "землю" платы и минус батарейного блока!
  • К датчику напряжения подключается ОДИН провод, если подключить два все сгорит, не нужно так! Вторая точка, относительно которой производится измерение это земля и именно для этого соединяется земля платы и минус батарейной сборки

Постановка задачи

Есть UPS/инвертор (24В): Файл:Ep3000-pro (1).pdf
К нему подключены 2 батареи по 100 Ач, 12Вб последовательно. RS232 или не работает или использует протокол с которым я не смог разобраться.
Для прогнозирования времени работы единственный способ получить данные - это мониторинг напряжения на аккумуляторах.

Вся схема собрана "на коленке" за час, из тех деталей что были в наличии на макетной плате.


Внешний вид, датчики закреплены канцелярскими булавками
Макетная плата, оранжевый - +5в, желтый - земля, Коричневый - SCL, Красный - SDA
RaspberryPi 1, 2011 года

Аппаратная часть

Для мониторинга использую то что есть под рукой а именно

  • Raspberry Pi Model 1 (самая старая, какая есть)
  • Датчики INA-219 (2шт используются, еще 2 на случай подключения большего числа батарей)

ina-219

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

Темплейт (очень упрощенный)

Исправленный и дополненный скрипт и 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

zabbix-agent-voltmeter.service
[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

Скрипт

Темплейт