Files
off/devices.py

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