- PVSM.RU - https://www.pvsm.ru -
Меня тут давно донимает вопрос снижения энергопотребления в квартире, так как ежемесячный расход электроэнергии каждый месяц переваливает за 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 [1] /usr/local/bin/pzem_wmctrl.py [1]
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 рублей каждый).
Но для меня это в первую очередь было получение опыта. Возможно кому-то пригодится этот опыт для более экономически целесообразных задач. Всем добра!

Автор: Maikl747
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/446411
Ссылки в тексте:
[1] wmctrl.py: http://wmctrl.py
[2] Источник: https://habr.com/ru/articles/1009480/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1009480
Нажмите здесь для печати.