136 lines
5.5 KiB
Python
136 lines
5.5 KiB
Python
"""Device management: check status and power off kids' devices."""
|
|
|
|
from datetime import datetime
|
|
|
|
import subprocess
|
|
|
|
|
|
def _run(cmd: list[str], *, timeout: int = 10) -> subprocess.CompletedProcess | None:
|
|
"""Run a command, return CompletedProcess or None on error."""
|
|
try:
|
|
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return None
|
|
|
|
|
|
# ── TV ────────────────────────────────────────────────────────────────
|
|
|
|
def tv_check() -> dict:
|
|
"""Check if the TV is reachable and powered on.
|
|
|
|
Returns an action dict describing the current state.
|
|
"""
|
|
result = _run(["ping", "-c", "1", "-W", "1", "192.168.1.12"])
|
|
if not result or result.returncode != 0:
|
|
return {"icon": "❌", "title": "TV", "detail": "Unreachable (ping failed)"}
|
|
|
|
_run(["adb", "connect", "192.168.1.12"])
|
|
result = _run(["adb", "shell", "dumpsys", "power"])
|
|
if result and "mWakefulness=Awake" in result.stdout:
|
|
return {"icon": "📺", "title": "TV", "detail": "Screen is on"}
|
|
|
|
return {"icon": "📺", "title": "TV", "detail": "Already off"}
|
|
|
|
|
|
def tv_turnoff() -> dict | None:
|
|
"""Send power key to the TV if it's on. Returns action dict or None."""
|
|
state = tv_check()
|
|
if state["detail"] != "Screen is on":
|
|
return None
|
|
|
|
_run(["adb", "shell", "input", "keyevent", "26"])
|
|
return {"icon": "📺", "title": "TV", "detail": "Screen turned off (power key sent)"}
|
|
|
|
|
|
# ── Gabi ──────────────────────────────────────────────────────────────
|
|
|
|
def gabi_check() -> bool:
|
|
"""Return True if user 'gabi' has an active login session."""
|
|
result = _run(["loginctl", "list-users"])
|
|
return result is not None and "gabi" in result.stdout
|
|
|
|
|
|
def gabi_turnoff() -> dict | None:
|
|
"""Log out Gabi if she's logged in. Returns action dict or None."""
|
|
if not gabi_check():
|
|
return {"icon": "💻", "title": "Gabi's PC", "detail": "Not logged in — no action needed"}
|
|
|
|
result = _run(["doas", "loginctl", "terminate-user", "gabi"])
|
|
if result and result.returncode == 0:
|
|
return {"icon": "💻", "title": "Gabi's PC", "detail": "Session terminated"}
|
|
|
|
return {"icon": "❌", "title": "Gabi's PC", "detail": "Failed to terminate session"}
|
|
|
|
|
|
# ── Gaja ──────────────────────────────────────────────────────────────
|
|
|
|
def gaja_check() -> bool:
|
|
"""Return True if user 'gaja' has an active login session on her PC."""
|
|
result = _run(["ping", "-c", "1", "-W", "1", "192.168.1.122"])
|
|
if not result or result.returncode != 0:
|
|
return False
|
|
|
|
check = _run(
|
|
["sshpass", "-p", "Nagaja", "ssh", "-o", "ConnectTimeout=5",
|
|
"-o", "StrictHostKeyChecking=no", "-F", "/dev/null",
|
|
"gaja@192.168.1.122",
|
|
"loginctl list-sessions --no-pager 2>/dev/null | grep -q gaja"]
|
|
)
|
|
return check is not None and check.returncode == 0
|
|
|
|
|
|
def gaja_turnoff() -> dict | None:
|
|
"""Log out Gaja if she's logged in. Returns action dict or None."""
|
|
# Ping check
|
|
result = _run(["ping", "-c", "1", "-W", "1", "192.168.1.122"])
|
|
if not result or result.returncode != 0:
|
|
return {"icon": "❌", "title": "Gaja's PC", "detail": "Unreachable (ping failed)"}
|
|
|
|
if not gaja_check():
|
|
return {"icon": "💻", "title": "Gaja's PC", "detail": "Not logged in — no action needed"}
|
|
|
|
_run(
|
|
["sshpass", "-p", "Nagaja", "ssh", "-o", "ConnectTimeout=5",
|
|
"-o", "StrictHostKeyChecking=no", "-F", "/dev/null",
|
|
"gaja@192.168.1.122",
|
|
"loginctl terminate-user gaja"],
|
|
timeout=5
|
|
)
|
|
return {"icon": "💻", "title": "Gaja's PC", "detail": "Logged out"}
|
|
|
|
|
|
# ── Orchestrator ──────────────────────────────────────────────────────
|
|
|
|
def shutdown_all() -> list[dict]:
|
|
"""Run all device checks and logouts. Returns ordered list of action dicts."""
|
|
actions = []
|
|
|
|
# Time window
|
|
now = datetime.now()
|
|
time_start = now.replace(hour=0, minute=0, second=0)
|
|
time_stop = now.replace(hour=23, minute=59, second=59)
|
|
|
|
if now < time_start:
|
|
diff = int((time_start - now).total_seconds())
|
|
actions.append({"icon": "⏳", "title": "Time Window", "detail": f"Before active hours — {diff}s until start"})
|
|
elif now > time_stop:
|
|
diff = int((now - time_stop).total_seconds())
|
|
actions.append({"icon": "⏳", "title": "Time Window", "detail": f"After active hours — {diff}s past end"})
|
|
else:
|
|
actions.append({"icon": "🟢", "title": "Active Hours", "detail": f"Executing for the next {time_stop - now}"})
|
|
# Check → act for each device
|
|
for check_fn, action_fn in [
|
|
(tv_check, tv_turnoff),
|
|
(gabi_check, gabi_turnoff),
|
|
(gaja_check, gaja_turnoff),
|
|
]:
|
|
status = check_fn()
|
|
if isinstance(status, dict):
|
|
actions.append(status) # e.g. unreachable
|
|
elif status is True:
|
|
result = action_fn()
|
|
if result:
|
|
actions.append(result)
|
|
|
|
return actions
|