Echo Dot: mi servidor atiende por la bocina
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í.

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:
Echo
Micrófono y bocina
Skill de Alexa
Cliente delgado en una Lambda
Cloudflare Access
Service token en el borde
Túnel
Sin puertos abiertos en el router
Gateway
FastAPI en mi servidor
Crear la skill, en corto
- 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.
- 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.

- 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.

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.

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:

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. fetchno me funcionó en el entorno hospedado. Usé el módulohttpsde Node y listo.JSON.stringify(error)imprime{}. Para ver qué falló, registraerror.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.

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".

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é.
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:
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:

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.
Referencias
"Sistemas simples, bien conectados, hacen más que sistemas complejos aislados."
