Move the off.sh script to a pure Python implementation -- devices.py.
This commit is contained in:
135
devices.py
Normal file
135
devices.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user