191 lines
6.0 KiB
Python
191 lines
6.0 KiB
Python
#!/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. Encender la regleta (comando Alexa via Home Assistant).
|
|
2. Si no responde -> apagar la regleta 1 min y volver a encenderla
|
|
(BIOS 'Restore on AC Power Loss = Power On' -> arranca solo).
|
|
3. Informar por voz (Carolina) tras cada comprobacion.
|
|
|
|
Nota: el Wake-on-LAN esta OMITIDO por defecto (no cruza el doble NAT);
|
|
se puede probar con --wol.
|
|
|
|
Uso:
|
|
python scripts/encender_sobremesa.py [--espera 120] [--wol] [--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
|
|
|
|
# Cargar variables de entorno (HOMEASSISTANT_URL/TOKEN) del repo de negocio.
|
|
try:
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv(infra_paths.biblioteca_repo() / ".env")
|
|
except Exception as _e: # pragma: no cover
|
|
print(f"[encender_sobremesa] Aviso: no se pudo cargar .env: {_e}", file=sys.stderr)
|
|
|
|
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("--wol", action="store_true", help="Intentar Wake-on-LAN primero (por defecto omitido).")
|
|
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
|
|
|
|
# Wake-on-LAN (opcional; por defecto OMITIDO para acelerar)
|
|
if args.wol:
|
|
print("-> Wake-on-LAN (opcional)...")
|
|
wol()
|
|
if esperar_encendido(args.espera):
|
|
aviso("El sobremesa se ha encendido con Wake on LAN.", voz)
|
|
return 0
|
|
|
|
# Encender regleta
|
|
print("-> Encender regleta (Alexa)...")
|
|
encender_regleta()
|
|
if esperar_encendido(args.espera):
|
|
aviso("El sobremesa se ha encendido al dar corriente la regleta.", voz)
|
|
return 0
|
|
|
|
# Ciclo de regleta (Power On Loss)
|
|
print("-> 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 la regleta ni por ciclo.", voz)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|