电源管理之捣腾家用 UPS

易雾君
发布于 2022-11-18 / 619 阅读
72
0

电源管理之捣腾家用 UPS

导读

家里的机械硬盘更换了一轮,现已是多块企业级机械存储,主要用作存储重要数据,最是害怕突然断电损害磁盘,丢失数据。隔壁的隔壁经常会通过拉电闸来检查自家电表的运转状态,实在没辙,购置一款家用级 UPS ,山特 SANTAK TG-BOX 850 ,功率 510W。

一睹设备芳容

右下角像插座的那个便是本文主角,考虑到家里的生产力机器、酷播云 2 期、软路由、及斐讯 k3 合计的功率大约在 100 多瓦,该款的功率 510W 完全够用。

IMG20221107224744

升级需求

购置的 UPS 山特 SANTAK TG-BOX 850 可通过 nut 进行驱动管理,可解决断电场景的重要系统关机问题,家里的系统均由普通 PC 进行承载,不适宜全天候开机,所以是定时通过网络唤醒,如果唤醒和断电独立,在断电触发关机后,有些机器可能关闭较快,而控制监控 ups 的这台机器还没关机,可能会唤醒已经关机的那些机器,从而导致断电触发关机失效,同样面临 UPS 电量骤减断电导致机器非正常关机,那么何不将定时唤醒及断电控制合并一起统一管理。

需求详细设计

设定一个电量百分比阀值 60% ,当电量达到阀值后仍有下滑趋势,执行关闭系统指令,优先关闭远程机器,这里通过调用自实现的远端API来关闭远程机器,最后再关闭自己;如果电量在阀值之上,属于供电安全期,执行唤醒任务,唤醒会设定固定的时间区间,不同机器可能需要唤醒的早晚程度不同,我的那台开机像炒豆子的,会在 8 时开始唤醒,安静那台会在 6 点就开始唤醒;还有种场景需要考虑,断电后来电,电量可能低于阀值,电量此时不会下滑,可判定供电处于安全期,UPS 蓄电池半天充不到阀值,迟迟无法触发唤醒系统,给于此种场景一个唤醒任务,确保其他机器在来电后无需人工干预即可进入工作状态;另外在断电触发关机前发送一封邮件告知管理员,也做留痕备案用。

环境准备

驱动 UPS ,安装 nut

apt update && apt install nut -y

/etc/nut/ups.conf 末尾追加如下内容

maxretry = 3
[tgbox850]
    driver=usbhid-ups
    port=auto
    desc="SANTAK TGBOX-850 UPS"

修改 /etc/nut/nut.conf 的模式为 standalone

MODE=standalone

启动服务

systemctl start nut-driver nut-server

查看 ups 电量

/bin/upsc tgbox850@localhost battery.charge

看到如下返回及表示配置成功

root@pve-vnet:~# /bin/upsc tgbox850@localhost battery.charge
Init SSL without certificate database
100

开机自启

systemctl enable nut-driver nut-server

安装网络唤醒工具 etherwake

apt update && apt install etherwake -y

源码分享

  • 关机 API 实现,在远端待控制关机的服务器上部署运行
# -*- coding: utf-8 -*-
# @File : reboot.py
# @Author : 易雾君
# @Time : 2021/8/28 8:31 AM
# @Email : [email protected]
# @Project : tools
# @Site : https://evling.tech
# @公众号 : 易雾山庄
# @Describe : 家庭基建,生活乐享.

from flask import Flask, jsonify, request
import os

app = Flask(__name__)

@app.route('/reboot', methods=['GET'])
def reboot():
    if request.method == 'GET':
        result = {'error': 0, 'msg': 'ok'}
        os.system('reboot')
        return jsonify(result)

@app.route('/shutdown', methods=['GET'])
def shutdown():
    if request.method == 'GET':
        result = {'error': 0, 'msg': 'ok'}
        os.system('shutdown -h now')
        return jsonify(result)

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=5000)

将如上代码保存为一个文件如:/data/tools/monitor/reboot.py
需要安装 pip 依赖包

apt update && apt install python3-pip
pip install flask

新增 systemd 守护服务文件,/etc/systemd/system/evling-reboot.service 内容如下

[Unit]
Description=Reboot
After=network.target

[Service]
ExecStart=/usr/bin/python3 /data/tools/monitor/reboot.py

# Restart every >2 seconds to avoid StartLimitInterval failure
RestartSec=5
Restart=always

[Install]
WantedBy=multi-user.target

开启开机自启服务

systemctl enable evling-reboot
  • 电源管理脚本实现,在连接 UPS 那台机器上部署
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Project : tools
# @File : power_manager.py
# @Software: PyCharm
# @Author : 易雾君
# @Email : [email protected]
# @公众号 : 易雾山庄
# @Site : https://www.evling.tech
# @Describe : 家庭基建,生活乐享. 
# @Time : 2022/11/12 20:02

import datetime
import subprocess
import time
import requests
import smtplib
from email.mime.text import MIMEText

class Mail:
    def __init__(self, username, password, host, port=465):
        self.mail_user = username
        self.mail_pass = password
        self.mail_host = host
        self.mail_port = port

    def send_email(self, to, subject, body, timeout=30):
        message = MIMEText(body, 'plain', 'utf-8')
        message['From'] = self.mail_user
        message['To'] = to
        message['Subject'] = subject
        try:
            with smtplib.SMTP_SSL(self.mail_host, self.mail_port, timeout=timeout) as server:
                server.set_debuglevel(1)
                server.login(self.mail_user, self.mail_pass)
                server.sendmail(self.mail_user, to, message.as_string())
                server.quit()
        except:
            pass

class PowerManager():
    def __init__(self):
        self.shutdown_charge = 60 # 供电安全阀值百分比
        self.charge = 0
        self.shutdown_urls = ['http://pve-prod.evling.tech/shutdown:5000', 'http://pve-nas.evling.tech/shutdown:5000']	# 远程关机 API 地址清单
        self.str_charge_cmd = '/bin/upsc tgbox850@localhost battery.charge'	# 查询 UPS 电量命令
        self.str_shutdown_cmd = '/sbin/shutdown -h now'
        self.etherwake_tpl = '/usr/sbin/etherwake -i {} {}'
        self.interfaces = ['vmbr0', 'vmbr10', 'vmbr11', 'vmbr12', 'vmbr8', 'vmbr9']		# 用于唤醒的网络接口,这里用的所有,避免变换网口后无法唤醒
        self.wake_list = [
            {'name': 'pve-prod', 'str_datetime_begin': '06:00', 'str_datetime_end': '23:00','mac': '70:xx:xx:xx:xx:e5'},
            {'name': 'pve-nas', 'str_datetime_begin': '08:00', 'str_datetime_end': '23:00', 'mac': '70:xx:xx:xx:xx:39'}
        ] # 机器唤醒的时间区间,及待唤醒的 mac 地址

    def set_charge(self):
        output = subprocess.getoutput(self.str_charge_cmd)
        for line in output.split('\n'):
            line = line.strip()
            try:
                self.charge = int(line)
            except:
                pass

    def shutdown(self, retry = 3, timeout=5):
        for url in self.shutdown_urls:
            for _ in range(retry):
                try:
                    requests.get(url, timeout=timeout)
                except:
                    pass
        subprocess.getoutput(self.str_shutdown_cmd)

    def etherwake(self, mac):
        for interface in self.interfaces:
            try:
                subprocess.getoutput(self.etherwake_tpl.format(interface, mac))
            except:
                pass

if __name__ == '__main__':
    # 关机区间,仍有下行趋势,则关闭系统,否则唤醒系统
    # 非关机区间,唤醒系统
    power_manager = PowerManager()
    mail = Mail('[email protected]', 'your_password', 'smtp.exmail.qq.com')
    downing_list = []
    shutdown_flag = 0
    num = 0
    while True:
        power_manager.set_charge()
        if ( power_manager.charge !=0 and power_manager.charge > power_manager.shutdown_charge ) or shutdown_flag == -1:
            downing_list = []
            num = 0
            for wake_item in power_manager.wake_list:
                datetime_now = datetime.datetime.now()
                datetime_begin = datetime.datetime.strptime('{} {}'.format(datetime_now.strftime('%Y-%m-%d'), wake_item.get('str_datetime_begin')), '%Y-%m-%d %H:%M')
                datetime_end = datetime.datetime.strptime('{} {}'.format(datetime_now.strftime('%Y-%m-%d'), wake_item.get('str_datetime_end')), '%Y-%m-%d %H:%M')
                if datetime_now >= datetime_begin and datetime_now <= datetime_end:
                    power_manager.etherwake(wake_item.get('mac'))
        if power_manager.charge !=0 and power_manager.charge <= power_manager.shutdown_charge:
            for downing_item in downing_list:
                if downing_item > power_manager.charge:
                    shutdown_flag = 1
                    break
            if shutdown_flag == 1:
                mail.send_email('[email protected]', '供电异常警告',
                                'UPS 剩余电量 {} %,正在关闭重要系统!\n\n—\nSANTAK TG-BOX 850'.format(power_manager.charge))
                power_manager.shutdown()
            if power_manager.charge not in downing_list:
                downing_list.append(power_manager.charge)
            if num > 100:  # 约等待300s没有电量下滑,则标识可以唤醒
                shutdown_flag = -1
            num += 1
        time.sleep(3)

将如上代码保存为一个文件如:/data/tools/monitor/power_manager.py
需要安装 pip 依赖包

apt update && apt install python3-pip
pip install requests

新增 systemd 守护服务文件,/etc/systemd/system/evling-powermanagement.service 内容如下

[Unit]
Description=Power Management
After=network.target

[Service]
ExecStart=/usr/bin/python3 /data/tools/monitor/power_manager.py

# Restart every >2 seconds to avoid StartLimitInterval failure
RestartSec=5
Restart=always

[Install]
WantedBy=multi-user.target

开启开机自启服务

systemctl enable evling-powermanagement

评论