#!/usr/bin/env python3 # -*- coding: utf-8 -*- """encender_sobremesa.py — Protocolo de encendido remoto del sobremesa. Escalera de intentos (acordada): 0. Estado de la regleta (best-effort). 1. Si el PC esta apagado -> intento 1: Wake-on-LAN. 2. Si no responde -> intento 2: encender la regleta (comando Alexa via Home Assistant). 3. Si no responde -> intento 3: apagar la regleta 1 min y volver a encenderla (BIOS 'Restore on AC Power Loss = Power On' -> arranca solo). 4. Informar por voz (Carolina) tras cada comprobacion. Uso: python scripts/encender_sobremesa.py [--espera 120] [--no-voz] """ from __future__ import annotations import argparse import glob import os import re import subprocess import sys import time from pathlib import Path _SCRIPTS_DIR = Path(__file__).resolve().parent if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) import infra_paths # noqa: E402 TS_EXE = os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"), "Tailscale", "tailscale.exe") NODO = "desktop-m8se61g" COS_ENCENDER = "enciende regleta" COS_APAGAR = "apaga regleta" MAC = "74:56:3C:CB:0E:CE" def pc_encendido() -> bool | None: """True/False segun Tailscale. None si no se puede determinar.""" try: out = subprocess.run([TS_EXE, "status"], capture_output=True, text=True, timeout=20).stdout except Exception: return None for linea in out.splitlines(): if NODO in linea: return "offline" not in linea return None def estado_regleta() -> bool | None: """Best-effort: True/False si se puede consultar el Tapo; None si no.""" try: from PyP100 import PyP100 as PP except Exception: return None repo = infra_paths.biblioteca_repo() creds = None for f in glob.glob(str(repo / "scripts" / "protocolo_apagado*.py")) + glob.glob(str(_SCRIPTS_DIR / "protocolo_apagado*.py")): try: s = open(f, encoding="utf-8", errors="replace").read() except Exception: continue m = re.search(r"P100\(\s*'([^']+)'\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", s) if m: creds = m.groups() break if not creds: return None try: d = PP.P100(*creds) d.handshake() d.login() return bool(d.getDeviceInfo().get("result", {}).get("device_on")) except Exception: return None def _comando_alexa(texto: str) -> bool: infra_paths.bootstrap_biblioteca_paths() try: from ha_alexa import command return command(texto) except Exception as e: print(f"[encender_sobremesa] Error llamando a HA: {e}", file=sys.stderr) return False def encender_regleta() -> bool: return _comando_alexa(COS_ENCENDER) def apagar_regleta() -> bool: return _comando_alexa(COS_APAGAR) def wol() -> None: """Magic packet (best-effort). Desde .50 no cruza a .1; se intenta igualmente.""" import socket paquete = b"\xff" * 6 + bytes.fromhex(MAC.replace(":", "")) * 16 for destino in ("192.168.1.255", "192.168.1.239", "255.255.255.255", "192.168.50.255"): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) s.sendto(paquete, (destino, 9)) s.close() print(f" magic packet -> {destino}:9") except Exception as e: print(f" magic packet -> {destino}:9 ERROR {e}") def esperar_encendido(segundos: int) -> bool: t = 0 while t < segundos: if pc_encendido(): return True time.sleep(10) t += 10 return False def aviso(texto: str, voz: bool = True) -> None: print(f"[aviso] {texto}") if not voz: return try: subprocess.run([sys.executable, str(_SCRIPTS_DIR / "voz_mensaje.py"), texto, "--tipo", "default"], check=False, timeout=120) except Exception: pass def main() -> int: ap = argparse.ArgumentParser(description="Encendido remoto del sobremesa.") ap.add_argument("--espera", type=int, default=120, help="Segundos por intento (defecto 120).") ap.add_argument("--no-voz", action="store_true", help="Sin avisos de voz.") args = ap.parse_args() voz = not args.no_voz reg = estado_regleta() print(f"Regleta: {'ENCENDIDA' if reg else 'APAGADA' if reg is False else 'desconocido'}") st = pc_encendido() print(f"PC: {'encendido' if st else 'apagado' if st is False else 'desconocido'}") if st: aviso("El sobremesa ya estaba encendido.", voz) return 0 # Intento 1 - Wake-on-LAN print(f"-> Intento 1: Wake-on-LAN...") wol() if esperar_encendido(args.espera): aviso("El sobremesa se ha encendido con Wake on LAN.", voz) return 0 # Intento 2 - encender regleta print("-> Intento 2: encender regleta (Alexa)...") encender_regleta() if esperar_encendido(args.espera): aviso("El sobremesa se ha encendido al dar corriente la regleta.", voz) return 0 # Intento 3 - ciclo de regleta (Power On Loss) print("-> Intento 3: apagar regleta 1 min y volver a encender (Power On Loss)...") apagar_regleta() time.sleep(60) encender_regleta() if esperar_encendido(args.espera): aviso("El sobremesa se ha encendido tras el ciclo de regleta. Power On Loss funciona.", voz) return 0 aviso("No se ha podido encender el sobremesa ni con WoL ni con la regleta.", voz) return 1 if __name__ == "__main__": raise SystemExit(main())