Меня тут давно донимает вопрос снижения энергопотребления в квартире, так как ежемесячный расход электроэнергии каждый месяц переваливает за 300 киловатт. В связи с этим решил понаблюдать за работой домашнего видеорегистратора. Для этих целей крутится небольшой сервачок (Debian Linux) на MiniITX с Ryzen 3 3200GE, который обслуживает несколько IP-камер и пишет их с помощью Xeoma (а также параллельно крутит Home Assistant).
Подключил умную розетку к этому устройству на месяц и выяснил, что устройство ежемесячно потребляет 64 киловатта.
Далее попытался понять, как мне снизить энергопотребление и выявил интересную особенность... К серверу подключён монитор, который в графическом интерфейсе отображает картинку с видеокамер. В таком режиме работы процессор нагружен на 80-90% по всем ядрам.

Однако если свернуть графический интерфейс программы Xeoma - нагрузка сразу падает до 50%.

Однако монитор не постоянно включен. Его включают лишь иногда, чтобы посмотреть какое-то событие с камер в реальном времени. А графический интерфейс программы молотит постоянно, вне зависимости от того, включен монитор или нет.
Решил поработать в этом направлении.
Сначала попытался свернуть графический интерфейс программы Xeoma c помощью консольной команды. Опытным путём выяснил, что сворачивает эту программу команда:
DISPLAY=:0 wmctrl -i -r 0x03000002 -b add,shaded
А разворачивает графический интерфейс команда:
DISPLAY=:0 wmctrl -i -r 0x03000002 -b remove,shaded
Далее мне необхjдимо, чтобы Linux как-то узнавал, включён ли монитор или нет. Сначала для этих целей я попробовал с помощью HDMI-CEC получить статус монитора по HDMI, но оказалось, что мониторы, как правило, не поддерживают CEC-команды (в отличие от телевизоров) и по HDMI получить информацию о мониторе нереально. Поэтому пошёл другим путём...
Приобрёл китайский однофазный измеритель мощности RS485 с названием PZEM-016.

Этот измеритель с помощью трансформатора тока с тороидальным сердечником получает информацию о токе, протекающем через фазовый провод потребителя электроэнергии и передаёт эту информацию по протоколу Modbus через интерфейс RS-485. Принципиально для меня было приобрести такой измеритель уже с USB-адаптером, чтобы не искать отдельный USB->RS485 адаптер.
Подключил этот USB-адаптер к серверу и он отобразился как
Bus 005 Device 003: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC
Затем изучил инструкцию и выяснил, как запрашивать параметры тока:
Связь:
Интерфейс: UART → RS485
Параметры: 9600, 8N1
Протокол: Modbus-RTU
CRC: 16 бит, полином 0xA001
Адрес устройства по умолчанию обычно 0x01
Диапазон: 0x01 – 0xF7
0x00 — broadcast
0xF8 — общий адрес (калибровка)
Чтение измерений:
Функция: 0x04 (Read Input Register)
Пример запроса:
01 04 00 00 00 0A CRC
(читать 10 регистров с 0x0000)
Основные регистры
Адрес
0x0000 Напряжение (0.1 В)
0x0001–0x0002 Ток (32 бита, 0.001 А)
0x0003–0x0004 Мощность (32 бита, 0.1 Вт)
0x0005–0x0006 Энергия (32 бита, 1 Wh)
0x0007 Частота (0.1 Гц)
0x0008 Cos φ (0.01)
0x0009 Статус тревоги
Далее я подключил трансформатор тока с тороидальным сердечником этого измерителя к фазовому проводу питания монитора, подключённого к серверу и написал простенький скрип мониторинга потребления:
import serial import struct PORT = "/dev/ttyUSB0" BAUD = 9600 # Modbus CRC16 def crc16(data): crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return struct.pack('<H', crc) # Команда чтения тока: 01 04 00 01 00 02 + CRC request = bytes([0x01, 0x04, 0x00, 0x01, 0x00, 0x02]) request += crc16(request) with serial.Serial(PORT, BAUD, timeout=1) as ser: ser.write(request) response = ser.read(9) if len(response) == 9: low = response[3] << 8 | response[4] high = response[5] << 8 | response[6] current_raw = (high << 16) | low current = current_raw / 1000.0 print(f"Current: {current:.3f} A") else: print("No response")
В результате выполнения работы скрипта получил следующие значения:
(монитор выключен)
Current: 0.034 A
(монитор включен)
Current: 0.135 A
(монитор включен)
Current: 0.119 A
(монитор выключен)
Current: 0.034 A
Далее написал полный скрипт, который раз в секунду запрашивает ток потребления монитора у измерителя и на основании этого разворачивает графический интерфейс программы видеонаблюдения или сворачивает его.
#!/usr/bin/env python3 import time import struct import subprocess import serial PORT = "/dev/ttyUSB0" BAUD = 9600 SLAVE = 0x01 WINDOW_ID = "0x03000002" # Порог/гистерезис (под твою картину: выкл ~0.034A, вкл ~0.12A) OFF_THRESHOLD_A = 0.06 # ниже -> считаем "выкл" ON_THRESHOLD_A = 0.08 # выше -> считаем "вкл" (чтобы не дрожало около порога) CMD_SHADE = ["wmctrl", "-i", "-r", WINDOW_ID, "-b", "add,shaded"] CMD_UNSHADE = ["wmctrl", "-i", "-r", WINDOW_ID, "-b", "remove,shaded"] def crc16(data: bytes) -> bytes: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): crc = (crc >> 1) ^ 0xA001 if (crc & 1) else (crc >> 1) return struct.pack("<H", crc) # little-endian (LOW, HIGH) def read_current_a(ser: serial.Serial): # Read Input Registers: start 0x0001, qty 0x0002 (current low/high) req = bytes([SLAVE, 0x04, 0x00, 0x01, 0x00, 0x02]) ser.reset_input_buffer() ser.write(req + crc16(req)) resp = ser.read(9) # 01 04 04 LL LL HH HH CRC CRC if len(resp) != 9 or resp[0] != SLAVE or resp[1] != 0x04 or resp[2] != 0x04: return None low = (resp[3] << 8) | resp[4] high = (resp[5] << 8) | resp[6] raw = (high << 16) | low return raw / 1000.0 def run_wmctrl(cmd): env = {"DISPLAY": ":0"} # важно для вызова из cron/systemd subprocess.run(cmd, env=env, check=False) def cpu_percent(prev): # prev: (idle, total) with open("/proc/stat", "r") as f: parts = f.readline().split() # cpu user nice system idle iowait irq softirq steal ... vals = list(map(int, parts[1:])) idle = vals[3] + (vals[4] if len(vals) > 4 else 0) # idle+iowait total = sum(vals) if prev is None: return None, (idle, total) idle0, total0 = prev didle = idle - idle0 dtotal = total - total0 if dtotal <= 0: return None, (idle, total) cpu = (1.0 - (didle / dtotal)) * 100.0 return cpu, (idle, total) def main(): state = None prev_cpu = None print("Started. Reading current every 1s...") with serial.Serial(PORT, BAUD, timeout=1) as ser: while True: cpu, prev_cpu = cpu_percent(prev_cpu) cur = read_current_a(ser) cpu_s = f"{cpu:5.1f}%" if cpu is not None else " --.-%" if cur is None: print(f"CPU {cpu_s} | No response") else: print(f"CPU {cpu_s} | Current {cur:.3f} A | State {state}") if state != "off" and cur < OFF_THRESHOLD_A: print("-> Monitor OFF → shading window") run_wmctrl(CMD_SHADE) state = "off" elif state != "on" and cur > ON_THRESHOLD_A: print("-> Monitor ON → unshading window") run_wmctrl(CMD_UNSHADE) state = "on" time.sleep(1) if __name__ == "__main__": main()
Далее смонтировал всю эту конструкцию "по красоте" вместе с сервером

После чего реализовал автозапуск скрипта в операционной системе (SysV) командами:
sudo install -m 755 pzem_wmctrl.py /usr/local/bin/pzem_wmctrl.py
sudo nano /etc/init.d/pzem_wmctrl
#!/bin/sh ### BEGIN INIT INFO # Provides: pzem_wmctrl # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: PZEM monitor -> wmctrl shade/unshade ### END INIT INFO DAEMON=/usr/bin/python3 SCRIPT=/usr/local/bin/pzem_wmctrl.py PIDFILE=/var/run/pzem_wmctrl.pid LOGFILE=/var/log/pzem_wmctrl.log USER=MKL start() { echo "Starting pzem_wmctrl..." start-stop-daemon --start --background --make-pidfile --pidfile "$PIDFILE" \ --chuid "$USER" --startas /bin/sh -- -c \ "DISPLAY=:0 $DAEMON $SCRIPT >> $LOGFILE 2>&1" } stop() { echo "Stopping pzem_wmctrl..." start-stop-daemon --stop --pidfile "$PIDFILE" --retry 5 rm -f "$PIDFILE" } case "$1" in start) start ;; stop) stop ;; restart) stop; start ;; status) if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then echo "Running (pid $(cat $PIDFILE))" else echo "Not running" exit 1 fi ;; *) echo "Usage: $0 {start|stop|restart|status}"; exit 2 ;; esac exit 0
sudo chmod +x /etc/init.d/pzem_wmctrl
sudo update-rc.d pzem_wmctrl defaults
sudo service pzem_wmctrl start
В результате работы этого скрипта у меня теперь графический интерфейс программы видеонаблюдения сворачивается и разворачивается вместе с включением монитора.
Цель задачи конечно выполнена, но я понимаю, что это экономически не совсем целесообразно. Так как измеритель тока в китае стоит 800+ рублей, а сэкономлю я в этом случае от силы 20 киловатт в месяц (по 5 рублей каждый).
Но для меня это в первую очередь было получение опыта. Возможно кому-то пригодится этот опыт для более экономически целесообразных задач. Всем добра!

