From 9b4afd9f9188825f3f54a1e947e964b81e31323f Mon Sep 17 00:00:00 2001 From: juanjo Date: Sun, 6 Sep 2026 16:05:52 +0200 Subject: [PATCH] 06-sep: [domotica] Mesillas Meross (comando + submenu) --- vigilancia/menu_idd.py | 29 +++++++++ vigilancia/meross_control.py | 114 +++++++++++++++++++++++++++++++++++ vigilancia/pi_bot.py | 23 ++++++- 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 vigilancia/meross_control.py diff --git a/vigilancia/menu_idd.py b/vigilancia/menu_idd.py index 4b94136..8d8eb8c 100644 --- a/vigilancia/menu_idd.py +++ b/vigilancia/menu_idd.py @@ -118,6 +118,7 @@ def teclado_domotica(): ], [ _btn("πŸ–₯️ Pantalla", PREFIJO + "cat:pantalla"), + _btn("πŸ›οΈ Mesillas", PREFIJO + "cat:mesillas"), ], FILA_VOLVER, ]) @@ -181,6 +182,21 @@ def teclado_pantalla(): ]) +def teclado_mesillas(): + return _mk([ + [_btn("πŸ“Š Estado mesillas", PREFIJO + "acc:mesilla:estado")], + [ + _btn("πŸ›οΈ Juanjo ON", PREFIJO + "acc:mesilla:juanjo:on"), + _btn("πŸ›οΈ Juanjo OFF", PREFIJO + "acc:mesilla:juanjo:off"), + ], + [ + _btn("πŸ›οΈ Nahid ON", PREFIJO + "acc:mesilla:nahid:on"), + _btn("πŸ›οΈ Nahid OFF", PREFIJO + "acc:mesilla:nahid:off"), + ], + [_btn("⬅️ DomΓ³tica", PREFIJO + "cat:domotica")], + ]) + + def teclado_tv(): return _mk([ [ @@ -424,6 +440,7 @@ def _header(categoria): "potencia": "⚑ Potencia aspiradora", "termo": "πŸ”Œ Termo", "pantalla": "πŸ–₯️ Pantalla", + "mesillas": "πŸ›οΈ Mesillas", "sistema": "πŸ› οΈ Sistema", "router": "πŸ“‘ Router ASUS", } @@ -469,6 +486,7 @@ def _teclado_categoria(chat_id, categoria): "potencia": teclado_potencia, "termo": teclado_termo, "pantalla": teclado_pantalla, + "mesillas": teclado_mesillas, "sistema": teclado_sistema, "router": teclado_router, } @@ -685,6 +703,17 @@ def _ejecutar_accion(chat_id, message_id, partes): "bloquear/desbloquear:", teclado_dispositivos(lista)) return + if accion == "mesilla": + # sub: estado | nombre | nombre:accion + rest = ":".join(partes[1:]) + trozos = rest.split(":") + if len(trozos) == 1 and trozos[0] == "estado": + _reabrir(chat_id, message_id, "mesillas", "meross estado") + elif len(trozos) == 2 and trozos[1] in ("on", "off"): + pi_bot.procesar(f"meross {trozos[0]} {trozos[1]}", chat_id) + _cerrar_menu(chat_id, message_id) + _fin_accion(chat_id, "mesillas") + return if accion == "tablets": if sub in ("bloquear", "desbloquear"): verbo = "desactivar" if sub == "bloquear" else "activar" diff --git a/vigilancia/meross_control.py b/vigilancia/meross_control.py new file mode 100644 index 0000000..c598299 --- /dev/null +++ b/vigilancia/meross_control.py @@ -0,0 +1,114 @@ +"""Enchufes Meross (mesillas) via cuenta Meross en la nube. + +Lee MEROSS_EMAIL/PASSWORD del .env. Solo lectura salvo on/off explicito. +Uso: python meross_control.py estado|on|off [nombre] +""" +import asyncio +import os +import ssl +import sys + +BASE = os.path.dirname(os.path.abspath(__file__)) +if BASE not in sys.path: + sys.path.insert(0, BASE) + + +def _env(nombre): + valor = os.getenv(nombre, "").strip() + if not valor: + try: + with open(os.path.join(BASE, ".env")) as f: + for linea in f: + linea = linea.strip() + if linea.startswith(nombre + "="): + valor = linea.split("=", 1)[1].strip() + break + except OSError: + pass + return valor + + +async def _dispositivos(): + from meross_iot.http_api import MerossHttpClient + from meross_iot.manager import MerossManager + http = await MerossHttpClient.async_from_user_password( + api_base_url="https://iotx-eu.meross.com", + email=_env("MEROSS_EMAIL"), password=_env("MEROSS_PASSWORD"), + ssl_context=ssl.create_default_context()) + mgr = MerossManager(http_client=http) + await mgr.async_init() + await mgr.async_device_discovery() + return mgr, http + + +def _estado_dev(d): + try: + v = getattr(d, "is_on", None) + if callable(v): + v = v() + if isinstance(v, bool): + return v + except Exception: + pass + return None + + +async def _actuar(nombre, accion): + mgr, http = await _dispositivos() + try: + objetivo = (nombre or "").lower() + elegidos = [d for d in mgr.find_devices() + if not objetivo or objetivo in (d.name or "").lower()] + if not elegidos: + return [f"❌ No encuentro '{nombre}' (solo mesillas)."] + lineas = [] + for d in elegidos: + try: + await d.async_update() + except Exception: + pass + for d in elegidos: + if accion == "on": + try: + await d.async_turn_on() + except Exception as e: + lineas.append(f"❌ {d.name}: {str(e)[:100]}") + continue + elif accion == "off": + try: + await d.async_turn_off() + except Exception as e: + lineas.append(f"❌ {d.name}: {str(e)[:100]}") + continue + try: + await d.async_update() + except Exception: + pass + on = _estado_dev(d) + txt = "encendido" if on else ("apagado" if on is False else "?") + lineas.append(f"πŸ›οΈ {d.name}: {txt}") + return lineas + finally: + mgr.close() + await http.async_logout() + + +def controlar(nombre, accion): + """Devuelve lista de lineas HTML con el resultado.""" + try: + return asyncio.run(_actuar(nombre or "", accion or "estado")) + except Exception as e: + return [f"❌ Error Meross: {str(e)[:150]}"] + + +def main(argv): + if len(argv) < 2 or argv[1] not in ("estado", "on", "off"): + print("Uso: meross_control.py estado|on|off [nombre]") + return 1 + for linea in controlar(argv[2] if len(argv) > 2 else "", argv[1]): + print(linea.replace("", "").replace("", "")) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/vigilancia/pi_bot.py b/vigilancia/pi_bot.py index 1af8e1b..c845084 100644 --- a/vigilancia/pi_bot.py +++ b/vigilancia/pi_bot.py @@ -530,6 +530,25 @@ def procesar(texto, chat_id): except Exception as e: log(f"{comando} error: {e}") enviar(f"❌ Error controlando {comando}: {e}", chat_id) + elif comando == "meross": + try: + from meross_control import controlar as meross_controlar + tok = argumento.lower().strip().split() + if not tok: + nombre, accion = "", "estado" + elif tok[0] in ("estado", "on", "off") and len(tok) == 1: + nombre, accion = "", tok[0] + else: + nombre = tok[0] + accion = tok[1] if len(tok) > 1 else "estado" + if accion not in ("estado", "on", "off"): + enviar("Uso: /meross [juanjo|nahid] estado|on|off.", chat_id) + else: + for linea in meross_controlar(nombre, accion): + enviar(linea, chat_id) + except Exception as e: + log(f"meross error: {e}") + enviar(f"❌ Error Meross: {e}", chat_id) elif comando == "router": partes_router = argumento.lower().split() accion_router = partes_router[0] if partes_router else "ayuda" @@ -811,7 +830,9 @@ def procesar(texto, chat_id): "10. termo on|off|estado β€” controla el enchufe Tapo\n" " ✏️ Ej: /termo estado\n" "10c. pantalla on|off|estado β€” enchufe de la pantalla\n" - " ✏️ Ej: /pantalla estado\n\n" + " ✏️ Ej: /pantalla estado\n" + "10d. meross [juanjo|nahid] estado|on|off β€” enchufes mesillas\n" + " ✏️ Ej: /meross estado\n\n" "━━━ 🏠 DOMΓ“TICA ━━━\n" "10b. aspiradora ... β€” Dreame D10 Plus\n" " ✏️ Ej: /aspiradora estado, /aspiradora vaciar, /aspiradora stats, /aspiradora potencia 3, /aspiradora agua 2, /aspiradora voz\n\n"