06-sep: [domotica] Mesillas Meross (comando + submenu)

This commit is contained in:
juanjo
2026-09-06 16:05:52 +02:00
parent 7f0769edd2
commit 9b4afd9f91
3 changed files with 165 additions and 1 deletions

View File

@@ -118,6 +118,7 @@ def teclado_domotica():
], ],
[ [
_btn("🖥️ Pantalla", PREFIJO + "cat:pantalla"), _btn("🖥️ Pantalla", PREFIJO + "cat:pantalla"),
_btn("🛏️ Mesillas", PREFIJO + "cat:mesillas"),
], ],
FILA_VOLVER, 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(): def teclado_tv():
return _mk([ return _mk([
[ [
@@ -424,6 +440,7 @@ def _header(categoria):
"potencia": "⚡ <b>Potencia aspiradora</b>", "potencia": "⚡ <b>Potencia aspiradora</b>",
"termo": "🔌 <b>Termo</b>", "termo": "🔌 <b>Termo</b>",
"pantalla": "🖥️ <b>Pantalla</b>", "pantalla": "🖥️ <b>Pantalla</b>",
"mesillas": "🛏️ <b>Mesillas</b>",
"sistema": "🛠️ <b>Sistema</b>", "sistema": "🛠️ <b>Sistema</b>",
"router": "📡 <b>Router ASUS</b>", "router": "📡 <b>Router ASUS</b>",
} }
@@ -469,6 +486,7 @@ def _teclado_categoria(chat_id, categoria):
"potencia": teclado_potencia, "potencia": teclado_potencia,
"termo": teclado_termo, "termo": teclado_termo,
"pantalla": teclado_pantalla, "pantalla": teclado_pantalla,
"mesillas": teclado_mesillas,
"sistema": teclado_sistema, "sistema": teclado_sistema,
"router": teclado_router, "router": teclado_router,
} }
@@ -685,6 +703,17 @@ def _ejecutar_accion(chat_id, message_id, partes):
"bloquear/desbloquear:", "bloquear/desbloquear:",
teclado_dispositivos(lista)) teclado_dispositivos(lista))
return 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 accion == "tablets":
if sub in ("bloquear", "desbloquear"): if sub in ("bloquear", "desbloquear"):
verbo = "desactivar" if sub == "bloquear" else "activar" verbo = "desactivar" if sub == "bloquear" else "activar"

View File

@@ -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}: <b>{txt}</b>")
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("<b>", "").replace("</b>", ""))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@@ -530,6 +530,25 @@ def procesar(texto, chat_id):
except Exception as e: except Exception as e:
log(f"{comando} error: {e}") log(f"{comando} error: {e}")
enviar(f"❌ Error controlando {comando}: {e}", chat_id) 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: <code>/meross [juanjo|nahid] estado|on|off</code>.", 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": elif comando == "router":
partes_router = argumento.lower().split() partes_router = argumento.lower().split()
accion_router = partes_router[0] if partes_router else "ayuda" accion_router = partes_router[0] if partes_router else "ayuda"
@@ -811,7 +830,9 @@ def procesar(texto, chat_id):
"<b>10.</b> <code>termo on|off|estado</code> — controla el enchufe Tapo\n" "<b>10.</b> <code>termo on|off|estado</code> — controla el enchufe Tapo\n"
" ✏️ Ej: <code>/termo estado</code>\n" " ✏️ Ej: <code>/termo estado</code>\n"
"<b>10c.</b> <code>pantalla on|off|estado</code> — enchufe de la pantalla\n" "<b>10c.</b> <code>pantalla on|off|estado</code> — enchufe de la pantalla\n"
" ✏️ Ej: <code>/pantalla estado</code>\n\n" " ✏️ Ej: <code>/pantalla estado</code>\n"
"<b>10d.</b> <code>meross [juanjo|nahid] estado|on|off</code> — enchufes mesillas\n"
" ✏️ Ej: <code>/meross estado</code>\n\n"
"━━━ 🏠 DOMÓTICA ━━━\n" "━━━ 🏠 DOMÓTICA ━━━\n"
"<b>10b.</b> <code>aspiradora ...</code> — Dreame D10 Plus\n" "<b>10b.</b> <code>aspiradora ...</code> — Dreame D10 Plus\n"
" ✏️ Ej: <code>/aspiradora estado</code>, <code>/aspiradora vaciar</code>, <code>/aspiradora stats</code>, <code>/aspiradora potencia 3</code>, <code>/aspiradora agua 2</code>, <code>/aspiradora voz</code>\n\n" " ✏️ Ej: <code>/aspiradora estado</code>, <code>/aspiradora vaciar</code>, <code>/aspiradora stats</code>, <code>/aspiradora potencia 3</code>, <code>/aspiradora agua 2</code>, <code>/aspiradora voz</code>\n\n"