with shutting down plex and raspberry if runtime falls below 300 and 200 secs

This commit is contained in:
2026-08-10 21:19:06 +02:00
parent e75c3c2358
commit a5b324bf66
3 changed files with 200 additions and 1 deletions
+129 -1
View File
@@ -33,12 +33,16 @@ NUT UPS name:
BX950MI
"""
import os
import asyncio
import json
import os
import sys
import time
import paramiko
import rrdtool
from dbus_next.aio import MessageBus
from dbus_next.constants import BusType
from PyNUTClient import PyNUT
from ntfy_notify import send_notification
@@ -53,6 +57,20 @@ RRD_DIR = "/var/lib/rrd"
RRD_FILE = os.path.join(RRD_DIR, "bx950mi.rrd")
ALERT_STATE_FILE = os.path.join(RRD_DIR, "bx950mi-alert-state.json")
SHUTDOWN_HOST = os.environ.get("UPS_SHUTDOWN_HOST", "plex.local")
SHUTDOWN_USER = os.environ.get("UPS_SHUTDOWN_USER", "ups")
SHUTDOWN_KEY_FILE = os.environ.get(
"UPS_SHUTDOWN_KEY",
"/opt/ups/.ssh/plex_shutdown",
)
SHUTDOWN_KNOWN_HOSTS = os.environ.get(
"UPS_SHUTDOWN_KNOWN_HOSTS",
"/opt/ups/.ssh/known_hosts",
)
SHUTDOWN_COMMAND = "sudo /usr/sbin/shutdown -h now"
REMOTE_SHUTDOWN_RUNTIME_SECONDS = 5 * 60
LOCAL_SHUTDOWN_RUNTIME_SECONDS = 200
GRAPH_DIR = "/var/www/html/ups"
HTML_FILE = os.path.join(GRAPH_DIR, "index.html")
WEEKLY_HTML_FILE = os.path.join(GRAPH_DIR, "week.html")
@@ -462,6 +480,62 @@ def crossed_below(previous, current, threshold):
)
def shutdown_remote_server(
host=SHUTDOWN_HOST,
user=SHUTDOWN_USER,
key_file=SHUTDOWN_KEY_FILE,
known_hosts=SHUTDOWN_KNOWN_HOSTS,
timeout=10,
):
"""Shut down a remote Linux server over restricted, verified SSH."""
client = paramiko.SSHClient()
client.load_host_keys(known_hosts)
client.set_missing_host_key_policy(paramiko.RejectPolicy())
try:
client.connect(
hostname=host,
username=user,
key_filename=key_file,
timeout=timeout,
banner_timeout=timeout,
auth_timeout=timeout,
look_for_keys=False,
allow_agent=False,
)
client.exec_command(SHUTDOWN_COMMAND, timeout=timeout)
finally:
client.close()
async def request_local_poweroff():
"""Request a local power-off through systemd-logind over D-Bus."""
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
try:
introspection = await bus.introspect(
"org.freedesktop.login1",
"/org/freedesktop/login1",
)
login = bus.get_proxy_object(
"org.freedesktop.login1",
"/org/freedesktop/login1",
introspection,
)
manager = login.get_interface("org.freedesktop.login1.Manager")
await manager.call_power_off(False)
finally:
bus.disconnect()
def shutdown_local_server():
"""Shut down the Linux server running this collector."""
asyncio.run(request_local_poweroff())
def process_alerts(current):
"""Notify on status changes and configured threshold crossings."""
@@ -472,6 +546,8 @@ def process_alerts(current):
current_charge = current.get("battery_charge")
previous_runtime = previous.get("battery_runtime")
current_runtime = current.get("battery_runtime")
shutdown_sent = previous.get("remote_shutdown_sent", False)
local_shutdown_sent = previous.get("local_shutdown_sent", False)
# An empty state is the initial baseline, not an alert condition.
if previous:
@@ -501,6 +577,56 @@ def process_alerts(current):
f"{current_runtime / 60:.1f} minutes"
)
shutdown_required = (
current_status in {"OB", "LB"}
and current_runtime is not None
and current_runtime < REMOTE_SHUTDOWN_RUNTIME_SECONDS
)
if shutdown_required and not shutdown_sent:
try:
shutdown_remote_server()
shutdown_sent = True
message = (
f"Remote shutdown requested for {SHUTDOWN_HOST}: "
f"UPS runtime is {current_runtime / 60:.1f} minutes"
)
print(message)
notify(message)
except Exception as exc:
print(
f"Remote shutdown of {SHUTDOWN_HOST} failed: {exc}",
file=sys.stderr,
)
local_shutdown_required = (
current_status in {"OB", "LB"}
and current_runtime is not None
and current_runtime < LOCAL_SHUTDOWN_RUNTIME_SECONDS
)
if local_shutdown_required and not local_shutdown_sent:
message = (
"Raspberry Pi shutdown requested: UPS runtime is "
f"{current_runtime:.0f} seconds"
)
notify(message)
try:
shutdown_local_server()
local_shutdown_sent = True
print(message)
except Exception as exc:
print(
f"Local shutdown failed: {exc}",
file=sys.stderr,
)
# A return to utility power starts a new shutdown-alert cycle.
if current_status == "OL":
shutdown_sent = False
local_shutdown_sent = False
# Keep the last valid reading when NUT temporarily omits a value.
state = {
"status": current_status or previous_status,
@@ -510,6 +636,8 @@ def process_alerts(current):
"battery_runtime": (
current_runtime if current_runtime is not None else previous_runtime
),
"remote_shutdown_sent": shutdown_sent,
"local_shutdown_sent": local_shutdown_sent,
}
save_alert_state(state)