Меня тут давно донимает вопрос снижения энергопотребления в квартире, так как ежемесячный расход электроэнергии каждый месяц переваливает за 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 рублей каждый).

Но для меня это в первую очередь было получение опыта. Возможно кому-то пригодится этот опыт для более экономически целесообразных задач. Всем добра!