Luxeon UPS: различия между версиями
Sirmax (обсуждение | вклад) |
Sirmax (обсуждение | вклад) |
||
| (не показано 19 промежуточных версий этого же участника) | |||
| Строка 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|thumb|left|Внешний вид, датчики закреплены канцелярскими булавками]] |
[[Файл:Luxeon Voltage 1.JPG|200px|thumb|left|Внешний вид, датчики закреплены канцелярскими булавками]] |
||
| − | [[Файл:Luxeon Voltage 2.jpg|200px|thumb|left|Макетная плата, оранжевый - +5в, желтый - земля, Коричневый - SCL, Красный - |
+ | [[Файл:Luxeon Voltage 2.jpg|200px|thumb|left|Макетная плата, оранжевый - +5в, желтый - земля, Коричневый - SCL, Красный - SDA]] |
[[Файл:Luxeon Voltage 3.jpg|200px|thumb|left|RaspberryPi 1, 2011 года]] |
[[Файл:Luxeon Voltage 3.jpg|200px|thumb|left|RaspberryPi 1, 2011 года]] |
||
| Строка 17: | Строка 31: | ||
Для мониторинга использую то что есть под рукой а именно |
Для мониторинга использую то что есть под рукой а именно |
||
* 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>, измерение происходит относительно |
+ | * Напряжение которое требуется измерять подключается на <code>'''+Vin'''</code>, измерение происходит относительно уровня земли |
* Датчики и RaspberryPi питаются от тех же 24В через DC-DC преобразователь с выходом USB (преобразователь подключен на клеммы батарей UPSa). |
* Датчики и RaspberryPi питаются от тех же 24В через DC-DC преобразователь с выходом USB (преобразователь подключен на клеммы батарей UPSa). |
||
<BR> |
<BR> |
||
| Строка 115: | Строка 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 |
||
| Строка 138: | Строка 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= |
||
Темплейт (очень упрощенный) |
Темплейт (очень упрощенный) |
||
| Строка 355: | Строка 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