Saltar al contenido principal

Echo Dot: mi servidor atiende por la bocina

· 11 min de lectura
Oscar Adrian Ortiz Bustos
Ingeniero en Gestión y Desarrollo de Software
Contando lecturas...

Introducción​

Tengo un Echo Dot desde hace tiempo y lo usaba de bocina. Música, el clima, un temporizador cuando se me quema algo. Un aparato con micrófono, internet y una nube gigante detrás, y yo pidiéndole canciones. Y para colmo, las mismas 4 de siempre.

Mientras tanto, en mi casa corre un servidor con decenas de contenedores al que nadie le hablaba más que yo, desde la terminal. Un sábado me dio por juntarlos: ¿y si la bocina fuera la puerta de entrada a mi servidor? Algo tan simple como decir "¿cuántos repos tengo?" y que la respuesta saliera de mi GitHub, no de un servicio genérico.

Este post es lo que probé y ajusté durante el fin de semana, con lo básico para que lo repitas con tu propio Echo. No salió a la primera, y lo que falló es de lo que más aprendí.

WelcomeBanner

La bocina solo escucha​

Casi todo aparato conectado ofrece una puerta distinta a la que dice la caja. El Echo es un micrófono en tu sala, cuarto o cocina, esperando a que digas algo... y no, no voy a empezar con mi lado paranoico. Lo que quiero es aprovechar esa entrada de audio para que mi servidor haga algo y que la respuesta salga por la bocina. La ruta es:

1

Echo

Micrófono y bocina

2

Skill de Alexa

Cliente delgado en una Lambda

3

Cloudflare Access

Service token en el borde

4

Túnel

Sin puertos abiertos en el router

5

Gateway

FastAPI en mi servidor

Crear la skill, en corto​

  1. Entras a la consola de desarrolladores de Alexa con la misma cuenta de Amazon de tu Echo, así la pruebas en el aparato real sin publicarla.

Consola de Alexa con el botón Create Skill

  1. Create Skill: idioma (yo, español de México), tipo Custom y hosting Alexa-hosted (Node.js). Amazon te da la Lambda y un repo, sin tocar AWS.

Primer paso para crear la skill: nombre e idioma

  1. En Build defines tres cosas: el nombre de invocación (lo que dices para abrirla), los intents (las acciones que sabe hacer) y sus frases de ejemplo. Mientras más formas de decir lo mismo, menos veces se pierde.

Pasos de Build: invocación, intents y modelo

Mi invocación es "mi casa" (necesita dos palabras o más y no puede llevar "alexa" ni "echo"). Antes se llamaba "asistente adrian", luego "mi servidor", y terminó en "mi casa" cuando la skill dejó de hablar solo de repos y empezó a hablarle también a mi despensa.

Nombre de invocación de la skill

Y un intent se ve así por dentro:

{
"name": "CountReposIntent",
"samples": ["cuántos repos tengo", "cuántos repositorios he creado"]
}

Estos son los intents de mi skill:

Lista de intents de la skill

Dale Save y Build Model; hasta que termine, los cambios no existen para Alexa. Para probarla, la pestaña Test trae un simulador, o se lo dices a tu Echo: "Alexa, abre mi casa" y luego "cuántos repos tengo". La mía contestó 63, mis repos públicos.

Consideraciones​

  • Si Alexa solo repite "Acabas de activar…" y el nombre del intent, tu handler no está registrado antes del IntentReflectorHandler, o su nombre no coincide.
  • fetch no me funcionó en el entorno hospedado. Usé el módulo https de Node y listo.
  • JSON.stringify(error) imprime {}. Para ver qué falló, registra error.stack.
  • Es una skill hospedada, por ende solo va a funcionar en tu cuenta de Amazon. Si quieres que otros la usen, tienes que publicarla y pasar por el proceso de revisión de Amazon.

Skill ligera, gateway pesado​

En vez de meter la lógica en la Lambda, la dejé como cliente delgado. El servicio es un gateway en Python con FastAPI, en un contenedor de mi servidor, con un contrato mínimo:

POST /alexa { "intent": "...", "slots": { ... } }
→ { "speech": "..." }

Así, agregar un comando nuevo es escribir una función en Python, sin tocar Alexa. Hoy contesta cuántos repos tengo, en qué proyectos ando (desde un endpoint de actividad que ya tenía) y qué día es. También habla con mi Grocy: qué hay en la despensa, qué caduca pronto, agregar algo a la lista de compras o qué receta puedo cocinar. Tres decisiones que valieron la pena:

  • Falla cerrado. Si el secreto no está configurado, el servicio no atiende a nadie.
  • Timeouts cortos. Una skill colgada es peor que una que dice "No pude consultar tu servidor en este momento".
  • Errores que se pueden hablar. Si algo falla, responde una frase que Alexa pueda leer, no un 500.

Cerrando la puerta​

Esa ruta queda expuesta a internet para que Amazon la llame, así que le puse dos candados independientes: un service token de Cloudflare Access en el borde, con el tráfico entrando por un túnel sin puertos abiertos en el router, y un secreto propio del gateway en otro header. Si alguien se salta el primero, el servicio revisa de nuevo y contesta 401.

Aquí me atoré un buen rato: mis peticiones respondían 302 hacia una pantalla de login, aun con el token bien. La política de Access tenía la acción Allow en lugar de Service Auth. Con Allow, Access espera que un humano inicie sesión; con Service Auth, reconoce el token y deja pasar la petición. Era cambiar un campo de un desplegable.

Política de Access con acción Service Auth

Los secretos de la skill viven en un secrets.js dentro del repo, porque una skill hospedada no tiene variables de entorno prácticas. Sé lo que estás pensando, y sí, lo sé: no es lo ideal. Es un trade-off consciente: el repo es privado y dejé escrito cómo rotar cada pieza. Eso sí, si algún día pasa a un repo público, ese archivo no puede ir con ella. Y los service tokens caducan, así que anota la fecha.

Y de regreso: que el servidor le hable al Echo​

Hasta aquí el Echo solo contesta cuando le preguntan. Yo quería lo contrario: que mi servidor pudiera decirle algo sin que yo abriera la skill. Un curl desde la terminal y que la bocina diga la frase. Mi prueba de aceptación fue una sola: que soltara un "Santa cachucha, ya dijo la frase".

Meme de Santa cachucha

Pensé que sería lo fácil, y no lo fue. Alexa no tiene una API oficial de "di esto ahora" para un servidor. Lo más cercano son los recordatorios, y al leer la documentación resulta que solo se pueden crear dentro de una sesión de voz, es decir, cuando ya le estás hablando a la skill. Fuera de sesión la API contesta NOT_IN_SESSION. Yo daba por hecho que servirían, con un retraso de un minuto como único costo. Lo primero era falso y lo segundo ni aparece en la documentación. Dos suposiciones, dos errores, y los vi por leer antes de construir.

Lo que sí funciona no es oficial: Home Assistant con la integración Alexa Media Player, que entra a tu cuenta de Amazon como si fuera la app y le manda órdenes al Echo. Tiene su costo: depende de una sesión de Amazon y nadie en Amazon te da soporte si se rompe. Para una bocina en mi casa lo acepté.

Advertencia

Esta es una integración no oficial. Amazon puede cambiar su API en cualquier momento y dejar de funcionar. No hay soporte oficial, y si se rompe, no hay garantía de que vuelva a funcionar.

Al terminar, el Echo aparece como un media_player y se crea un servicio de notificación. La primera prueba fue directo contra la API de Home Assistant, con un token de acceso de larga duración:

El Echo Dot como media_player en Home Assistant

curl -X POST "$HA_URL/api/services/notify/alexa_media" \
-H "Authorization: Bearer $HA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "Qué onda", "target": ["media_player.tu_echo_dot"], "data": {"type": "tts"}}'

La API contesta 200 aunque no salga audio, así que la única prueba real es oírlo. Se oyó.

Una puerta más en el gateway​

En vez de que cada script mío le hable a Home Assistant, lo puse detrás del gateway, igual que lo demás: un puerto (SpeechPort), un adaptador que llama a Home Assistant y una ruta nueva.

POST /say { "text": "Santa cachucha, ya dijo la frase" }
→ { "status": "ok" }

Usa el mismo secreto propio que /alexa. El texto se limpia y se limita a 250 caracteres, y los errores se pueden predecir: 401 sin secreto, 422 con texto inválido, 503 si Home Assistant no está configurado y 502 si falla. El token de Home Assistant y el texto hablado no se escriben en los logs, y si el token no está, el gateway arranca igual y solo /say contesta 503.

curl -X POST "$GATEWAY_URL/say" \
-H "X-Gateway-Secret: $GATEWAY_SECRET" \
-H "Content-Type: application/json" \
-d '{"text": "Santa cachucha, ya dijo la frase"}'

Contestó 200 y el Echo dijo la frase, esta vez pasando por el gateway y no directo contra Home Assistant.

Quizá ahora no le veas mucho potencial, porque, sí, solo es hacer hablar a mi Echo Dot. Pero sé que esto es una puerta para algo más grande: que me avise cuando se abra un pull request en mi repo, cuando falle el deploy de mi app o cuando se abra la puerta de mi casa. Todo sin sacar el teléfono, solo escuchando a la bocina. Interesante, ¿no?

Así queda todo junto. En ámbar va el camino de entrada, la voz que llega al gateway; en verde, la ruta de vuelta que acabamos de armar; y en naranja, lo que no es oficial y se puede romper:

Mapa de la integración: Echo, skill, gateway, Grocy, Home Assistant, HACS y la sesión de Amazon

En el diagrama también aparece Grocy, el inventario de mi despensa, que el mismo gateway consulta cuando se lo pido por voz. Eso es otra historia, para otro post. Pero sí, ahora tengo un ERP para la despensa que habla con mi Echo Dot.

Lo que me llevo​

  • Mantener la skill delgada. La lógica en mi propio servicio la pruebo con curl; la que vive dentro de la consola de Alexa, ni de broma.
  • Lo que expones, lo cierras con más de un candado. Un servicio expuesto sin protección es invitación para el primer minero de criptomonedas que pase.
  • Lo oficial no siempre tiene la puerta que buscas. Alexa no ofrece un "habla ahora" para servidores y los recordatorios solo se crean en sesión. Lee la documentación antes de elegir la ruta, y si tomas la no oficial, hazlo sabiendo qué se puede romper.

Conclusión​

Mi Echo Dot sigue poniendo música, pero ya no es solo eso. Es un micrófono conectado a mi servidor, con una puerta que solo abre con mis credenciales, y desde hace poco también funciona al revés: mi servidor le puede decir algo a la bocina con un curl. Y lo mejor: para agregar un comando nuevo ya no toco la skill, solo escribo una función en Python.

Con /say ya puedo hablarle al Echo desde un curl. Lo que sigue es que la bocina avise sola, sin que yo teclee nada. Home Assistant puede recibir un webhook, así que GitHub (o cualquier servicio que sepa mandar uno) le avisa, una automatización decide qué hacer, y el Echo lo dice en voz alta. Por ejemplo, "Se abrió un pull request en tu repo" o "Falló el deploy", sin sacar el teléfono. Todavía no lo armo, pero en cuanto lo haga, te cuento en otro post.

"Sistemas simples, bien conectados, hacen más que sistemas complejos aislados."

— Adaptado de John Gall