Luxeon UPS: различия между версиями
Sirmax (обсуждение | вклад) |
Sirmax (обсуждение | вклад) (→Скрипт) |
||
Строка 377: | Строка 377: | ||
=Исправленный и дополненный скрипт и template для Zabbix= |
=Исправленный и дополненный скрипт и template для Zabbix= |
||
+ | |||
− | ==Скрипт== |
||
* <code>UserParameter-voltage.conf</code> (часть <code>UserParameter=voltage[*] ... </code> не используется ) |
* <code>UserParameter-voltage.conf</code> (часть <code>UserParameter=voltage[*] ... </code> не используется ) |
||
<PRE> |
<PRE> |
||
Строка 383: | Строка 383: | ||
UserParameter=voltage.discovery,/usr/bin/cat /etc/zabbix/scripts/voltages.json | tee -a /var/log/zabbix-agent-debug.log |
UserParameter=voltage.discovery,/usr/bin/cat /etc/zabbix/scripts/voltages.json | tee -a /var/log/zabbix-agent-debug.log |
||
</PRE> |
</PRE> |
||
+ | |||
+ | ==Скрипт== |
||
{{#spoiler:show=voltage_new.py| |
{{#spoiler:show=voltage_new.py| |
||
<PRE> |
<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> |
</PRE> |
||
}} |
}} |
Версия 09:58, 1 августа 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