53 lines
2.3 KiB
Python
53 lines
2.3 KiB
Python
#!/usr/bin/python3
|
||
"""At midnight, arm a 06:00 RTC alarm and request an orderly shutdown."""
|
||
import argparse
|
||
import datetime as dt
|
||
from pathlib import Path
|
||
import shutil
|
||
import subprocess
|
||
TZ = dt.timezone(dt.timedelta(hours=8), 'Asia/Shanghai')
|
||
ALARM = Path('/sys/class/rtc/rtc0/wakealarm')
|
||
|
||
|
||
def wake_time(now):
|
||
target = now.astimezone(TZ).replace(hour=6, minute=0, second=0, microsecond=0)
|
||
return target if target > now else target + dt.timedelta(days=1)
|
||
|
||
|
||
def power_cycle(now, check=False):
|
||
target = wake_time(now)
|
||
print(f'当前时间:{now.astimezone(TZ).isoformat(timespec="seconds")}', flush=True)
|
||
print(f'计划唤醒:{target.isoformat(timespec="seconds")}', flush=True)
|
||
rtcwake = shutil.which('rtcwake')
|
||
systemctl = shutil.which('systemctl')
|
||
if not rtcwake or not systemctl or not ALARM.exists():
|
||
raise RuntimeError('未发现 RTC 唤醒接口或必要命令,本次不执行关机。')
|
||
if check:
|
||
print('检查完成;没有设置闹钟,也没有执行关机。', flush=True)
|
||
return
|
||
local = now.astimezone(TZ)
|
||
if local.hour != 0 or local.minute >= 5:
|
||
raise RuntimeError('此任务仅允许在北京时间 00:00–00:04 执行;本次不关机。')
|
||
# This server stores its hardware clock in UTC (timedatectl LocalRTC=no).
|
||
subprocess.run([rtcwake, '--utc', '--device', '/dev/rtc0', '--mode', 'no',
|
||
'--time', str(int(target.timestamp()))], check=True, timeout=20)
|
||
alarm = int(ALARM.read_text().strip() or '0')
|
||
if abs(alarm - target.timestamp()) > 2:
|
||
raise RuntimeError('RTC 唤醒时间回读不一致,本次不执行关机。')
|
||
print('RTC 闹钟已确认,开始正常关机。正在运行的计算任务将停止。', flush=True)
|
||
subprocess.run([systemctl, '--no-block', 'poweroff'], check=True, timeout=20)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description='每天 00:00 关机,设置当天 06:00 RTC 唤醒。')
|
||
parser.add_argument('--check', action='store_true', help='仅检查环境和时间,不设置闹钟、不关机')
|
||
args = parser.parse_args()
|
||
try:
|
||
power_cycle(dt.datetime.now(TZ), args.check)
|
||
except (RuntimeError, OSError, ValueError, subprocess.SubprocessError) as exc:
|
||
parser.exit(1, f'夜间节电计划未完成:{exc}\n')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|