Luxeon UPS: различия между версиями
Sirmax (обсуждение | вклад) |
Sirmax (обсуждение | вклад) |
||
(не показано 25 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
[[Категория:Linux]] |
[[Категория:Linux]] |
||
+ | [[Категория:UPS]] |
||
+ | [[Категория:I2C]] |
||
+ | [[Категория:RaspberryPi]] |
||
+ | [[Категория:Monitoring]] |
||
+ | [[Категория:INA219]] |
||
+ | [[Категория:Zabbix]] |
||
+ | |||
=Мониторинг напряжения на UPS Luxeon= |
=Мониторинг напряжения на UPS Luxeon= |
||
+ | |||
+ | =Прежде чем начать= |
||
+ | * В примере в схеме <big>'''общий минус'''</big> так как сама плата Raspberry питается от тех же батарей. Если это не так, и плата питается от 220 через блок питания, то нужно соединить "землю" платы и минус батарейного блока! |
||
+ | |||
+ | * К датчику напряжения подключается <big>'''ОДИН провод,'''</big> если подключить два все сгорит, не нужно так! Вторая точка, относительно которой производится измерение это земля и именно для этого соединяется земля платы и минус батарейной сборки |
||
=Постановка задачи= |
=Постановка задачи= |
||
Строка 10: | Строка 22: | ||
Вся схема собрана "на коленке" за час, из тех деталей что были в наличии на макетной плате. |
Вся схема собрана "на коленке" за час, из тех деталей что были в наличии на макетной плате. |
||
<BR> |
<BR> |
||
+ | |||
− | [[Файл:Luxeon Voltage 1.JPG|200px|1]] |
||
+ | |||
− | <BR> |
||
− | [[Файл:Luxeon Voltage |
+ | [[Файл:Luxeon Voltage 1.JPG|200px|thumb|left|Внешний вид, датчики закреплены канцелярскими булавками]] |
− | [[Файл:Luxeon Voltage |
+ | [[Файл: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 |
+ | * 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 возможных адреса) |
||
Строка 114: | Строка 131: | ||
=Интеграция с системой мониторинга Zabbix= |
=Интеграция с системой мониторинга Zabbix= |
||
− | Для того что бы избежать возможных проблем данные снимаются регулярно, раз в минуту используя <code>cron</code> |
+ | Для того что бы избежать возможных проблем данные снимаются регулярно, раз в минуту используя <code>cron</code>: |
− | <code>cat /etc/cron.d/voltage</code> |
+ | <code>cat /etc/cron.d/voltage</code><BR> |
+ | (за одно не нужно давать <code>sudo</code> пользователю zabbix) |
||
<PRE> |
<PRE> |
||
* * * * * root /etc/zabbix/scripts/voltage_0x40.py > /etc/zabbix/scripts/voltage_24v |
* * * * * root /etc/zabbix/scripts/voltage_0x40.py > /etc/zabbix/scripts/voltage_24v |
||
Строка 137: | Строка 155: | ||
* <code>-s 192.168.29.29 </code> - адрес заббикс-агента, IP адрес RaspberryPi куда подключены датчики |
* <code>-s 192.168.29.29 </code> - адрес заббикс-агента, IP адрес RaspberryPi куда подключены датчики |
||
* <code>-k ups.luxeon.voltage_12v_level</code> имя ключа |
* <code>-k ups.luxeon.voltage_12v_level</code> имя ключа |
||
+ | |||
=Zabbix Template= |
=Zabbix Template= |
||
Темплейт (очень упрощенный) |
Темплейт (очень упрощенный) |
||
Строка 354: | Строка 373: | ||
</graphs> |
</graphs> |
||
</zabbix_export> |
</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, "Item voltage.discovery") |
||
+ | Zabbix.Log(1, JSON.stringify(records)) |
||
+ | Zabbix.Log(1, "END of Item voltage.discovery") |
||
+ | 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 == "{#VOLTMETER_NAME}")].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 == "{#VOLTMETER_NAME}")].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, "Discovery Rule") |
||
+ | Zabbix.Log(1, JSON.stringify(records)) |
||
+ | Zabbix.Log(1, "END OF Discovery Rule") |
||
+ | return value</params> |
||
+ | </step> |
||
+ | </preprocessing> |
||
+ | </discovery_rule> |
||
+ | </discovery_rules> |
||
+ | <tags> |
||
+ | <tag> |
||
+ | <tag>Application</tag> |
||
+ | <value>Power</value> |
||
+ | </tag> |
||
+ | </tags> |
||
+ | </template> |
||
+ | </templates> |
||
+ | </zabbix_export> |
||
+ | |||
</PRE> |
</PRE> |
||
}} |
}} |
Текущая версия на 16:24, 13 августа 2024
Мониторинг напряжения на UPS Luxeon
Прежде чем начать
- В примере в схеме общий минус так как сама плата Raspberry питается от тех же батарей. Если это не так, и плата питается от 220 через блок питания, то нужно соединить "землю" платы и минус батарейного блока!
- К датчику напряжения подключается ОДИН провод, если подключить два все сгорит, не нужно так! Вторая точка, относительно которой производится измерение это земля и именно для этого соединяется земля платы и минус батарейной сборки
Постановка задачи
Есть UPS/инвертор (24В): Файл:Ep3000-pro (1).pdf
К нему подключены 2 батареи по 100 Ач, 12Вб последовательно. RS232 или не работает или использует протокол с которым я не смог разобраться.
Для прогнозирования времени работы единственный способ получить данные - это мониторинг напряжения на аккумуляторах.
Вся схема собрана "на коленке" за час, из тех деталей что были в наличии на макетной плате.
Аппаратная часть
Для мониторинга использую то что есть под рукой а именно
- 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
Темплейт (очень упрощенный)
Исправленный и дополненный скрипт и 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