tldr: ИБП использует Modbus (а не Megatec/Voltronic), параметры BAUDRATE = 9600, DEVICE_ID = 10, START_ADDR = 30000.

У меня была задача - купить ИБП для домашней системы хранения / homelab сервера.

Идея была купить что-то не слишком дорогое, с возможностью подключения внешних АКБ и мониторинга через стандартный протокол NUT (Network UPS Tools, стандартный протокол по которому можно мониторить UPS в linux, NAS типа Synology и т.д.) .

Выбор пал на Энергия Smart.2 1000 Вт. К слову, perplexity среди бюджетных вариантов не нашел его, помогли собственные знания.

В DNS он стоил 13 тыс, на Озон была куплена LiFePo4 100 Ah батарея еще за 20к, и комплект был готов.

Сразу скажу, что у него есть возможность выбора АКБ в параметре 14 - Pb или Li, так вот выбирать Li не надо. В этом режиме он держит постоянно absorption (14.5 В), не уходя во float.

Легко подключить к компу ИБП не удалось.

По версии perpelexity, в этом ИБП используется протокол Megatec / Voltronic и вопрос в подборе скоростей порта и других параметров. Когда перебор всех вариантов не привел к результату, стал разбираться с платой, которая конверирует UART в USB.

Разобрал ИБП, вынул плату (лишился гарантии попутно, оборвав пломбу), прозвонил входящий шлейф, выяснил где оптопары, побился с подачей напряжения через флаги DTR/RTS, ничего не помогло.

Был уже готов покупать отдельный UART - USB адаптер.

Решил дать еще один шанс ПО, которое указано на сайте производителя. В поделку под названием "SolarPowerMonitor2.2.81" веры было мало (какой нафиг Solar?), но не под виртуальной, а bare metal Windows, ИБП завелся.

ИБП Энергия в Solar Power Monitor
ИБП Энергия в Solar Power Monitor

Дальше все было просто.

Я: Так, я смог подключился к ИБП через ПО SolarPowerMonitor 2.2.81.

ИБП определяется как Ep2000Pro. В настройках этого ПО указано что EPSeries подключается с devices id = 10, Baudrate = 9600, machinename Address = 30000. Давай поймем что это такое и по какому протоколу с ним нужно работать.


Perp: Это просто колоссальный прорыв! Вы только что взломали главную загадку этого ИБП. Забудьте всё, что мы обсуждали про протоколы Megatec/Voltronic, скорости 2400 бод и команды Q1. Ваш ИБП «Энергия Smart.2» — это OEM-клон инвертора MUST EP2000 PRO. А устройства компании MUST (как и многие современные солнечные инверторы) общаются не простым текстом, а по промышленному протоколу Modbus RTU.

Прям называть Энергию "клоном" я бы не стал (как минимум другие токи зарядки), но вдохновлялись явно Must.

Дальше было делом техники понять, что мониторить по modbus встроенные в NUT драйвера не могут, и нужно использовать dummy-ups драйвер, который будет читать параметры UPS из файла.

Пример конфига:

sudo tee /etc/nut/ups.conf <<EOF
[energy_ups]
    driver = dummy-ups
    port = /var/lib/nut/energy_ups.dev
    desc = "Energy Smart.2 1000"
EOF

А в файл /var/lib/nut/energy_ups.dev будет писать отдельный скрипт, запущенный (например) в виде службы.

Пользователя, который будет запускать скрипт нужно добавить в группу dialout (ubuntu) или uucp (arch).

Он же должен мочь (chown) писать в /var/lib/nut.

Текст скрипта для мониторинга на python ниже. Если запускать его как "./script.py print" - выведет данные в консоль.

Скрытый текст
#!/usr/bin/env python3

from pymodbus.client import ModbusSerialClient
from pymodbus.framer import FramerType
import os
import time
import sys

# Проверяем, запущен ли скрипт с аргументом "print"
ENABLE_PRINT = len(sys.argv) > 1 and sys.argv[1] == "print"

#Set battery data
BATTERY_CAPACITY_AH = 100
BATTERY_VOLTAGE_NOMINAL = 12
BATTERY_TYPE = "LiFePO4"

#file to write data for NUT
dummy_file = "/var/lib/nut/energy_ups.dev"
tmp_file = "/var/lib/nut/energy_ups.dev.tmp"

# how often to take data from UPS
POLL_INTERVAL = 2

PORT = '/dev/ttyUSB0'
BAUDRATE = 9600
DEVICE_ID = 10
START_ADDR = 30000
# max readable = 25
COUNT = 20

SOC_100_VOLTAGE = 13.6
SOC_0_VOLTAGE = 12

# Своя функция печати, которая выводит текст только если передан аргумент 'print'
def debug_print(msg):
    if ENABLE_PRINT:
        print(msg)

# write UPS data to file (NUT will read this file)
def write_to_file(data_string):
    # Записываем во временный файл
    with open(tmp_file, "w") as f:
        f.write(data_string)

    # Атомарно заменяем старый файл новым
    os.replace(tmp_file, dummy_file)

def read_ups_modbus():
    # Инициализация Modbus RTU клиента (самый новый синтаксис)
    client = ModbusSerialClient(
        port=PORT,
        baudrate=BAUDRATE,
        framer=FramerType.RTU, # Указываем тип фреймера через enum
        timeout=1,
        parity='N',
        stopbits=1,
        bytesize=8
    )

    print(f"Запуск службы мониторинга ИБП...")

    # 2. БЕСКОНЕЧНЫЙ ЦИКЛ ОПРОСА
    while True:
        try:
            # Проверяем, открыт ли порт. Если нет - пытаемся открыть.
            if not client.connected:
                print(f"Подключение к {PORT}...")
                if client.connect():
                    print(f"Подключено к {PORT}...")
                else:
                    print("Не удалось подключиться. Повтор через 5 секунд...")
                    time.sleep(5)
                    continue

            debug_print("Читаем регистры (Holding)...")
            # Читаем 20 регистров начиная с 30000 для устройства с ID=10
            response = client.read_holding_registers(address=START_ADDR, count=COUNT, device_id=DEVICE_ID)

            if response.isError():
                print("Ошибка Holding Registers. Пробуем Input Registers...")
                response = client.read_input_registers(address=START_ADDR, count=COUNT, device_id=DEVICE_ID)

            if not response.isError():
                debug_print("\n=== ДАННЫЕ ПОЛУЧЕНЫ ===")
                for i, val in enumerate(response.registers):
                    real_address = START_ADDR + i
                    debug_print(f"Регистр {real_address}: {val}")

                status = response.registers[2]
                rated_w = response.registers[4]
                batt_v = response.registers[14] / 10.0
                batt_soc = response.registers[17]
                batt_current = response.registers[15] / 10.0
                in_v = response.registers[5] / 10.0
                in_freq = response.registers[6] / 10.0
                out_v = response.registers[7] / 10.0
                out_freq = response.registers[8] / 10.0
                temp = response.registers[18]
                load_p = response.registers[12]
                load_power = response.registers[10]

                # calc real SOC
                real_soc = (batt_v - SOC_0_VOLTAGE) / (SOC_100_VOLTAGE - SOC_0_VOLTAGE) * 100

                # SOC min / max levels for NUT
                if real_soc > 100: real_soc = 100
                if real_soc < 0: real_soc = 0

                # --- ФОРМИРУЕМ СТАТУС ДЛЯ NUT ---
                status_flags = []
                if status == 4:
                    status_flags.append("OL")  # От сети
                elif status == 3:
                    status_flags.append("OB")  # От батареи
                else:
                    status_flags.append("WAIT")
                
                # Дополнительные флаги
                if batt_current > 0 and "OL" in status_flags:
                    status_flags.append("CHRG") # Заряжается
                
                # Если батарея садится - даем сигнал Low Battery (чтобы сервер завершил работу)
                if real_soc <= 10 and "OB" in status_flags:
                    status_flags.append("LB")
                    
                ups_status_string = " ".join(status_flags)

                # Вывод на экран
                data_to_show=f"""status: {status} (3 = on battery, 4 = online)
batt_v: {batt_v}
batt_current: {batt_current}
batt_soc: {batt_soc}% / real {real_soc:.0f}%
load_power: {load_power}
load_percent: {load_p}%
in_v: {in_v} / {in_freq}
out_v: {out_v} / {out_freq}
temp: {temp}
rated_w: {rated_w}
"""
                debug_print(f"{data_to_show}")

                # --- ЗАПИСЬ В ФАЙЛ ---
                data_to_write = f"""device.mfr: Energy Smart.2
device.model: {rated_w}W
ups.status: {ups_status_string}
ups.power.nominal: {rated_w}
ups.realpower.nominal: {rated_w}
ups.load: {load_p}
ups.realpower: {load_power}
ups.temperature: {temp}
battery.capacity: {BATTERY_CAPACITY_AH}
battery.type: {BATTERY_TYPE}
battery.voltage.nominal: {BATTERY_VOLTAGE_NOMINAL}
battery.charge: {real_soc:.0f}
battery.voltage: {batt_v}
battery.current: {batt_current}
input.voltage: {in_v}
input.frequency: {in_freq}
output.voltage: {out_v}
output.frequency: {out_freq}
"""
                write_to_file(data_to_write)

                debug_print(f"Данные обновлены: {ups_status_string} | {batt_v}V | {in_v}V")
            else:
                print(f"ИБП ответил ошибкой Modbus: {response}")

                # Если ИБП перестал отвечать, закроем соединение, 
                # чтобы на следующем цикле оно переоткрылось заново
                client.close() 

        except Exception as e:
            print(f"Системная ошибка (отвалился USB?): {e}")
            client.close()
            time.sleep(5) # Пауза перед попыткой переподключения
        
        # Ждем 1 секунду до следующего опроса
        time.sleep(POLL_INTERVAL)

if __name__ == '__main__':
    read_ups_modbus()

P.S. Так же по NUT этот же UPS можно пробросить в любые другие компы / NAS. Или в Home Assistant, чтобы было например так:

Скрытый текст