Мониторинг и управление APC Smart UPS 2000

В общем, удалось запустить и связать ИБП c виртуальным устройством WB , делюсь инструкцией и скриптами, вдруг , кому пригодится.

1. Раздобыть или спаять кабель по схеме вашей модели, самый распространенный (и подходит на всю серию APC SURT) 940-024C:


2. Настроить Docker (если не настроен уже)

3. Создать (где удобно) директорию и файл конфигурации docker-compose.yml (от root пользователя):

docker-compose.yml

services:
  app:
    image: ${REGISTRY_URI:-instantlinux}/nut-upsd:latest
    restart: always
    environment:
      API_USER: apcmon #произвольный пользователь к API
      API_PASSWORD: apcmon #произвольный пароль к API
      DRIVER: apcsmart #ваш драйвер ИБП из https://networkupstools.org/stable-hcl.html
      NAME: APC #произвольное имя устройства
      PORT: /dev/ttyUSB0 #ваш порт! Сразу при подключении USB можно глянуть dmseg | grep tty
      SDORDER: -1 #отключение автовыключения ИБП
    ports:
    - ${PORT_UPSD_1:-3493}:3493
    privileged: true

  nut:
    image: 2mqtt/nut:0.0.3
    restart: always
    environment:
      - MQTT_ID=nut
      - MQTT_PATH=nut #название раздела в MQTT, куда будет публикация
      - MQTT_HOST=mqtt://192.168.2.54 #IP адрес MQTT сервиса, совпадает с адресом контроллера WB
      - MQTT_USERNAME=mqttuser #пользователь для авторизации в MQTT, если настроена авторизация в mosquitto
      - MQTT_PASSWORD=mqttuserpwd #пароль для авторизации в MQTT, если настроена авторизация в mosquitto
      - NUT_HOST=192.168.2.54 #IP адрес сервера, на котором развернут NUT, т.е. локальный адрес данного сервера
      - NUT_USERNAME=apcmon #должен соответствовать пользователю NUT API_USER
      - NUT_PASSWORD=apcmon #должен соответствовать паролю NUT API_PASSWORD
      - NUT_INTERVAL=60000 # интервал опроса в мс

5. Поднять контейнеры:

docker compose up -d --build

image

6. Есть нюанс, связанный с тем, что nut2mqtt модуль написан криво, и если его поднимать одновременно с Nut - работать не будет, поэтому надо после поднятия (и при каждой перезагрузке) рестартануть контейнер nut2mqtt:

docker restart nut-nut-1

И вставить это в cron (crontab -e), чтобы срабатывало при перезагрузке:

@reboot /bin/sleep 60 ; sudo docker restart nut-nut-1

7. Настроить конвертер в виртуальное устройство WB
Заходим в настройку правил и добавляем новый файл, process_apc_mqtt.js:

var devName = "APC";
var mqttBase = "nut/APC/";
var regBattery = "battery";
var regInput = "input";
var regOutput = "output";
var regStatus = "status";

defineVirtualDevice(devName, 
                    {title: "Smart-UPS RT 2000 XL",
                     cells: {
                       "input.frequency": {type: "value", "units": "Hz", "title": "Линия.Частота", value: -1, "order": 1},
                       "input.quality": {type: "text", "title": "Линия.Качество", value: 'Н/Д', "order": 2},
                       "input.sensitivity": {type: "text", "title": "Линия.Чувствительность", value: "Н/Д", "order": 3},
                       "input.transferHigh": {type: "value", "units": "V", "title": "Линия.Высокое напряжение переключения", value: -1, "order": 4},
                       "input.transferLow": {type: "value", "units": "V", "title": "Линия.Низкое напряжение переключения", value: -1, "order": 5},
                       "input.transferReason": {type: "text", "title": "Линия.Причина переключения", value: "Н/Д", "order": 6},
                       "input.voltage": {type: "value", "units": "V", "title": "Линия.Напряжение", value: -1, "order": 7},
                       "input.voltageMaximum": {type: "value", "units": "V", "title": "Линия.Макс напряжение", value: -1, "order": 8},
                       "input.voltageMinimum": {type: "value", "units": "V", "title": "Линия.Мин напряжение", value: -1, "order": 9},

                       "battery.alarmThreshold": {type: "value", "units": "min", "title": "Батарея.Тревога по оставшимся минутам", value: -1, "order": 10},
                       "battery.charge": {type: "value", "units": "%", "title": "Батарея.Заряд", value: -1, "order": 11},
                       "battery.chargeRestart": {type: "value", "units": "%", "title": "Батарея.Мин процент заряда для включения", value: -1, "order": 12},
                       "battery.date": {type: "text", "title": "Батарея.Дата замены", value: "Н/Д", "order": 13},
                       "battery.packs": {type: "value", "title": "Батарея.Количество комплектов", value: -1, "order": 14},
                       "battery.packsBad": {type: "value", "title": "Батарея.Количество плохих комплектов", value: -1, "order": 15},
                       "battery.runtime": {type: "value", "units": "s", "title": "Батарея.Запас времени", value: -5, "order": 16},
                       "battery.runtimeLow": {type: "value", "units": "s", "title": "Батарея.Отключение при запасе", value: -1, "order": 17},
                       "battery.voltage": {type: "value", "units": "V", "title": "Батарея.Напряжение", value: -1, "order": 18},
                       "battery.voltageNominal": {type: "value", "units": "V", "title": "Батарея.Номинал напряжения", value: -1, "order": 19},
                       
                       "output.voltage": {type: "value", "units": "V", "title": "Выход.Напряжение", value: -1, "order": 20},
                       "output.voltageNominal": {type: "value", "units": "V", "title": "Выход.Номинал напряжения", value: -1, "order": 21},
                       
                       "status.mode": {type: "text", "title": "Статус.Режим", value: -1, "order": 22},
                       "status.beeper": {type: "switch", "title": "Статус.Сигнал", value: false, "order": 23},
                       "status.temperature": {type: "value", "units": "deg C", "title": "Статус.Температура", value: -1, "order": 24},
                       "status.load": {type: "value", "units": "%", "title": "Статус.Нагрузка", value: -1, "order": 25}
                     }
                    });

function setDevRegister(message) {
  //log.info("name: {}, value: {}".format(message.topic, message.value))
  regBase = /[^/]*$/.exec(message.topic)[0]+'.';
  if (message.value != '') {
    JSON.parse(message.value, function(n, p) {
      curVal = dev[devName+'/'+regBase+n];
      curValType = typeof(curVal);
      if ((n) && (curValType !== 'undefined')) {
        var v;
        switch(curValType) {
          case "number": v = parseInt(p); break;
          case "float": v = parseFloat(p); break;
          case "string": v = p; break;
          case "boolean": v = (p === 'true'); break;
          default: log('unknown curValType = ' + curValType); v = p;
        }
        dev[devName+'/'+regBase+n] = v;
      }
    });
  }
}

trackMqtt(mqttBase + regInput, setDevRegister);
trackMqtt(mqttBase + regBattery, setDevRegister);
trackMqtt(mqttBase + regOutput, setDevRegister);
trackMqtt(mqttBase + regStatus, setDevRegister);

Вуаля, получаем связь:

4 лайка