Conectar tu asistente local a un altavoz

Conectar tu asistente local a un altavoz

En esta guía vamos a conectar tu asistente local a un altavoz para mejorar la calidad de audio de sus respuestas.

¿Por qué conectar tu asistente local a un altavoz?

Desde que empezamos a jugar con Assist hemos visto diferentes alternativas para utilizar el asistente local de Home Assistant. Desde dispositivos personalizados utilizando MicroWakeWord, a dispositivos prefabricados como el M5Stack CoreS3 SE, el ESP32-S3-BOX-3 o el Atom Echo.

Todos ellos nos permiten interactuar con Assist y experimentar las ventajas de tener un asistente local. Suelen ser dispositivos caseros o con pequeños altavoces integrados que no ofrecen la mejor calidad de audio. No obstante, si ya tienes un altavoz más potente integrado en Home Assistant (tanto un altavoz local, como un Google Home Mini o un Alexa) podemos utilizarlos para escuchar las respuestas de nuestro asistente. Esto nos va a permitir mejorar notablemente la calidad del audio y la experiencia con nuestro asistente.

Requisitos previos

Para conectar tu asistente local a un altavoz externo necesitas, por tanto:

*Algún precio puede haber cambiado desde la última revisión
🥑 Si estás configurando Assist, te recomiendo que veas el taller de la academia para sacarle el máximo partido!
taller assist

Conexión del asistente al altavoz

A grandes rasgos, lo que vamos a hacer es modificar el código en ESPHome de nuestro asistente para que, cuando nos de una respuesta, la reproduzca por nuestros altavoces externos. Entendido esto, para conectar tu asistente local a un altavoz sigue estos pasos:

  1. En Home Assistant, ve a Herramientas para desarrolladores > Estados y busca ‘media_player’. Esto te ofrecerá el listado de entidades multimedia que tienes disponible para utilizar como salida de audio. Por ejemplo, en mi caso voy a utilizar la entidad ‘media_player.googlehome5013’ de un Google Home Mini.
⚠️ Si planeas utilizar a Alexa como altavoz externo, es importante que leas el siguiente apartado "Excepciones al caso general"
  1. Entra en el complemento de ESPHome y pulsa «Edit» en el bloque correspondiente al dispositivo de tu asistente local, para acceder al código de la configuración.
⚠️ Si planeas utilizar el M5Stack CoreS3 SE como asistente, es importante que leas el siguiente apartado "Excepciones al caso general"
  1. Ahora tienes que bajar por el código hasta localizar la siguiente parte. Aunque el código puede variar dependiendo del tipo de dispositivo que estés utilizando, lo importante es que entiendas que esta instrucción (‘on_tts_start:’) es la que determina las acciones que suceden cuando tu asistente empieza a «hablar».
  on_tts_start:
⚠️ Si no encuentras esta línea en tu código, es importante que leas el siguiente apartado "Excepciones al caso general"
  1. Lo que vamos a hacer es añadir una nueva acción, que llame a la acción de Home Assistant que utilizamos para reproducir notificaciones por nuestros altavoces, de la siguiente manera.
  on_tts_start:
    - text_sensor.template.publish:
        id: text_response
        state: !lambda return x;
    - homeassistant.service:
        service: tts.speak
        data:
          media_player_entity_id: media_player.googlehome5013
          message: !lambda 'return x;'
          entity_id: tts.elevenlabs_tts
  1. Analiza el código anterior y ten en cuenta lo siguiente:
    • Mediante la expresión ‘!lambda return x’ captamos la respuesta del asistente, para reproducirla por nuestros altavoces.
    • Tienes que adaptar el ‘media_player_entity_id‘, indicando la de tu altavoz (según localizaste en el paso 1).
    • Tienes que adaptar el ‘entity_id’ por el motor ‘tts’ (Text-To-Speech) que utilices (en mi caso ElevenLabs).
  2. Una vez hayas modificado el código como corresponda, pulsa en «Save» e «Install». Como ya tienes tu dipositivo configurado, podrás utilizar la opción «Wirelessly» para actualizar la configuración.
  1. ¿Ya los has probado y no funciona? No te aceleres! Para que tu dispositivo de ESPHome pueda llamar a HA para ejecutar acciones, primero tenemos que darle premiso. Ve a Ajustes > Dispositivos y servicios > ESPHome. Pulsa sobre el enlace “Configurar” correspondiente al dispositivo de tu asistente. En la ventana emergente marca la casilla “Permitir que el dispositivo realice llamadas de servicio de Home Assistant” y pulsa en “Enviar”. Esto va a permitir que el asistente emita el audio por otros altavoces.

Excepciones al caso general

Quiero utilizar a Alexa

Si vas a utilizar un altavoz de Alexa, necesitas previamente la integración de Alexa Media Player. Además las notificaciones por Alexa se realizan utilizando una acción diferente (‘notify.alexa_media’). Por lo tanto en el paso 4 del apartado anterior tienes que utilizar la siguiente variante del código (adaptando tu entidad):

  on_tts_start:
    - text_sensor.template.publish:
        id: text_response
        state: !lambda return x;
    - homeassistant.service:
        service: notify.alexa_media_echo923
        data:
          message: !lambda 'return x;'

No encuentro la línea ‘on_tts_start’

Como te decía, el código de la configuración de cada dispositivo puede variar, por lo que es posible que no encuentres esta línea (por ejemplo, si usas el M5Stack CoreS3 SE). No pasa nada, la podemos crear. Para ello tienes que seguir estos pasos:

  1. Entra en el complemento de ESPHome y pulsa «Edit» en el bloque correspondiente al dispositivo de tu asistente local, para acceder al código de la configuración.
  2. Justo después de la línea ‘captive_portal’, copia lo siguiente:
captive_portal:
    
text_sensor:
  - platform: template
    name: "Response"
    id: text_response
    internal: true
  1. Localiza la siguiente línea en el código. Esta instrucción determina las acciones que suceden justo después de que tu asistente escuche lo que le has pedido.
  on_listening:
  1. Copia las siguientes líneas dentro de la instrucción. Esto sirve para captar la respuesta del asistente, de forma que posteriormente la podamos utilizar para enviarla por nuestros altavoces.
  on_listening:
    - text_sensor.template.publish:
        id: text_response
        state: "..."
  1. Ahora vamos a crear a instrucción ‘on_tts_start’, para lanzar la respuesta de nuestro asistente por nuestros altavoces. Para ello copia las siguientes líneas a la misma altura que el bloque anterior (‘on_listening’).
  on_tts_start:
    - text_sensor.template.publish:
        id: text_response
        state: !lambda return x;
    - homeassistant.service:
        service: tts.speak
        data:
          media_player_entity_id: media_player.googlehome5013
          message: !lambda 'return x;'
          entity_id: tts.elevenlabs_tts
  1. Hecho esto ya puedes continuar con el paso 5 del apartado anterior.

Silenciar el altavoz del dispositivo

Algo que es totalmente opcional, aunque personalmente me parece recomendable, es silenciar el altavoz del propio dispositivo una vez que ya has conseguido la salida de audio por tus altavoces externos. De esta forma evitarás ese molesto eco. Aunque no he encontrado una solución «universal», a continuación he recogido algunos casos que he probado de forma satisfactoria.

Framework Arduino

Si bien todos los asistentes funcionan sobre placas ESP, no todos utilizan el mismo ‘framework’ (pudiendo ser ‘arduino’ o ‘esp-idf’). No voy a entrar en detalle sobre esto, pero si tu dispositivo utiliza el framework de Arduino (por ejemplo, el M5Stack CoreS3 SE) podemos reemplazar el componente del altavoz por una entidad multimedia para controlar el volumen del altavoz.

Lo primero es comprobar el framework que utiliza el dispositivo. Esto lo encontrarás al principio del código, en estas líneas:

esp32:
  framework:
    type: arduino
⚠️ Si tu dispositivo utiliza el framework 'esp-idf', no lo intentes. Sencillamente no te va a funcionar, y probablemente ni si quiera puedas compilar el código.

Si este es tu caso, sigue estos pasos:

  1. Entra en el complemento de ESPHome y pulsa “Edit” en el bloque correspondiente al dispositivo de tu asistente local, para acceder al código de la configuración.
  2. Localiza el bloque de líneas que comienza con ‘speaker’. Vamos a comentar estas líneas (utilizando el símbolo ‘#’ al principio), para desactivar el componente del altavoz, y en su lugar sustituirlo por el de ‘media_player’ de la siguiente forma:
#speaker:
#  - platform: m5cores3_audio
#    m5cores3_audio_id: m5cores3_audio_1
#    id: m5cores3_spk
#    dac_type: external
#    i2s_dout_pin: 13
#    mode: mono

i2s_audio:
  i2s_lrclk_pin: GPIO0
  i2s_bclk_pin: GPIO5 
   
media_player:
  - platform: i2s_audio
    name: speaker
    id: m5cores3_spk_media
    dac_type: external
    i2s_dout_pin: 13
    mode: mono
⚠️ Observa que la asignación de pines corresponde al dispositivo M5Stack CoreS3 SE. Si utilizas uno diferente es posible que tengas que consultar su 'pinout' para asignarlos correctamente.
  1. Sigue bajando hasta localizar el bloque de líneas que comienza con ‘voice_assistant’. De nuevo, vamos a comentar las líneas correspondientes al componente ‘speaker’ y añadir el de ‘media_player’. Además comentaremos las líneas correspondientes a ‘on_tts_stream_start’ y ‘on_tts_stream_end’ (porque requieren el componente ‘speaker’) y en su lugar vamos a usar ‘on_tts_start’ y ‘on_tts_end’.
  2. A continuación puedes ver cómo debería quedar tu código (sólo incluyo las líneas que tienes que modificar de alguna manera).
voice_assistant:
  media_player: m5cores3_spk_media
  #speaker: m5cores3_spk
  on_tts_start:
    - media_player.volume_set:
        id: m5cores3_spk_media
        volume: 0%
    - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
    - text_sensor.template.publish:
        id: text_response
        state: !lambda return x;
    - homeassistant.service:
        service: tts.speak
        data:
          media_player_entity_id: media_player.googlehome5013
          message: !lambda 'return x;'
          entity_id: tts.google_es_es
    - script.execute: draw_display
  on_tts_end:
    - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
    - script.execute: draw_display
  #on_tts_stream_start:
  #  - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
  #  - script.execute: draw_display
  #on_tts_stream_end:
  #  - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
  #  - script.execute: draw_display

ESP32-S3-BOX-3

Este dispositivo utiliza el framework ‘esp-idf’ por lo que para poder silenciar el altavoz he modificado su código de configuración. En concreto he importado un componente ‘custom’ para ESPHome y después he adaptado la secuencia del asistente para que siga funcionando correctamente, inhabilitando el altavoz.

A continuación te dejo el código completo con mi versión modificada, en el que he incluido comentarios para que sepas qué partes he inhabilitado, y cuáles he añadido.

---
substitutions:
  name: esp32-s3-box-3
  friendly_name: ESP32 S3 Box 3
  loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png
  idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png
  listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png
  thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png
  replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png
  error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png
  timer_finished_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/timer_finished_320_240.png

  loading_illustration_background_color: "000000"
  idle_illustration_background_color: "000000"
  listening_illustration_background_color: "FFFFFF"
  thinking_illustration_background_color: "FFFFFF"
  replying_illustration_background_color: "FFFFFF"
  error_illustration_background_color: "000000"

  voice_assist_idle_phase_id: "1"
  voice_assist_listening_phase_id: "2"
  voice_assist_thinking_phase_id: "3"
  voice_assist_replying_phase_id: "4"
  voice_assist_not_ready_phase_id: "10"
  voice_assist_error_phase_id: "11"
  voice_assist_muted_phase_id: "12"
  voice_assist_timer_finished_phase_id: "20"

  # These unique characters have been extracted from every test file of every language available on https://github.com/home-assistant/intents (14 March 2024)
  allowed_characters: " !#%'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћאבגדהוזחטיכלםמןנסעפץצקרשת،ءآأإئابةتجحخدذرزسشصضطظعغفقكلمنهوىيٹپچڈکگںھہیےংকচতধনফবযরলশষস়ািু্చయలిెొ్ംഅആഇഈഉഎഓകഗങചജഞടഡണതദധനപഫബഭമയരറലളവശസഹാിീുൂെേൈ്ൺൻർൽൾაბგდევზთილმნოპრსტუფქყშჩცძჭხạảấầẩậắặẹẽếềểệỉịọỏốồổỗộớờởợụủứừửữựỳ—、一上不个中为主乾了些亮人任低佔何作供依侧係個側偵充光入全关冇冷几切到制前動區卧厅厨及口另右吊后吗启吸呀咗哪唔問啟嗎嘅嘛器圍在场執場外多大始安定客室家密寵对將小少左已帘常幫幾库度庫廊廚廳开式後恆感態成我戲戶户房所扇手打执把拔换掉控插摄整斯新明是景暗更最會有未本模機檯櫃欄次正氏水沒没洗活派温測源溫漏潮激濕灯為無煙照熱燈燥物狀玄现現瓦用發的盞目着睡私空窗立笛管節簾籬紅線红罐置聚聲脚腦腳臥色节著行衣解設調請謝警设调走路車车运連遊運過道邊部都量鎖锁門閂閉開關门闭除隱離電震霧面音頂題顏颜風风食餅餵가간감갔강개거게겨결경고공과관그금급기길깥꺼껐꼽나난내네놀누는능니다닫담대더데도동됐되된됨둡드든등디때떤뜨라래러렇렌려로료른를리림링마많명몇모무문물뭐바밝방배변보부불블빨뽑사산상색서설성세센션소쇼수스습시신실싱아안않알았애야어얼업없었에여연열옆오온완외왼요운움워원위으은을음의이인일임입있작잠장재전절정제져조족종주줄중줘지직진짐쪽차창천최추출충치침커컴켜켰쿠크키탁탄태탬터텔통트튼티파팬퍼폰표퓨플핑한함해했행혀현화활후휴힘,?"

  micro_wake_word_model: okay_nabu

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  min_version: 2024.7.0
  name_add_mac_suffix: true
  platformio_options:
    board_build.flash_mode: dio
  on_boot:
    priority: 600
    then:
      - script.execute: draw_display
      - delay: 30s
      - if:
          condition:
            lambda: return id(init_in_progress);
          then:
            - lambda: id(init_in_progress) = false;
            - script.execute: draw_display

esp32:
  board: esp32s3box
  flash_size: 16MB
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
      CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
      CONFIG_AUDIO_BOARD_CUSTOM: "y"
      CONFIG_ESP32_S3_BOX_3_BOARD: "y"
    components:
      - name: esp32_s3_box_3_board
        source: github://jesserockz/esp32-s3-box-3-board@main
        refresh: 0s

psram:
  mode: octal
  speed: 80MHz

external_components:
  - source: github://pr#5230
    components: esp_adf
    refresh: 0s
  - source: github://jesserockz/esphome-components
    components: [file]
    refresh: 0s
# Con estas líneas añadimos el componente externo para crear la entidad multimedia
  - source:
      type: git
      url: https://github.com/gnumpi/esphome_audio
      ref: dev-next
    components: [ adf_pipeline, i2s_audio ]
    refresh: 0s
###################################################################################

api:
  encryption:
    key: "bg6sfdsfddXPb02hh0qnQeYVwfgh56E8BP5o="
  on_client_connected:
    - script.execute: draw_display
  on_client_disconnected:
    - script.execute: draw_display

ota:
  - platform: esphome
    password: "29efojsdjdsjs65a338sdfsd43"

logger:
  hardware_uart: USB_SERIAL_JTAG

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Assist Fallback Hotspot"
    password: "3SZsasdgoP4"
  on_connect:
    - script.execute: draw_display
  on_disconnect:
    - script.execute: draw_display

button:
  - platform: factory_reset
    id: factory_reset_btn
    internal: true

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO0
      mode: INPUT_PULLUP
      inverted: true
    id: top_left_button
    internal: true
    on_multi_click:
      - timing:
          - ON for at least 50ms
          - OFF for at least 50ms
        then:
          - switch.turn_off: timer_ringing
      - timing:
          - ON for at least 10s
        then:
          - button.press: factory_reset_btn

output:
  - platform: ledc
    pin: GPIO47
    id: backlight_output

light:
  - platform: monochromatic
    id: led
    name: Screen
    icon: "mdi:television"
    entity_category: config
    output: backlight_output
    restore_mode: RESTORE_DEFAULT_ON
    default_transition_length: 250ms

# Con estas líneas cambiamos el componente original por la entidad multimedia

#esp_adf:
#  board: esp32s3box3

#microphone:
#  - platform: esp_adf
#    id: box_mic

#speaker:
#  - platform: esp_adf
#    id: box_speaker

i2s_audio:
  - id: i2s_shared
    i2s_lrclk_pin: GPIO45
    i2s_bclk_pin: GPIO17
    i2s_mclk_pin: GPIO2
    access_mode: duplex
i2c:
  - id: bus_a
    sda: GPIO08
    scl: GPIO18
    scan: false
    sda_pullup_enabled: true
    scl_pullup_enabled: true
    frequency: 100kHz

adf_pipeline:
  - platform: i2s_audio
    type: audio_out
    id: adf_i2s_out
    i2s_audio_id: i2s_shared
    i2s_dout_pin: GPIO15
    adf_alc: false
    dac:
      i2c_id: bus_a
      model: es8311
      address: 0x18
      enable_pin: GPIO46
    sample_rate: 16000
    bits_per_sample: 16bit
    fixed_settings: true

  - platform: i2s_audio
    type: audio_in
    id: adf_i2s_in
    i2s_audio_id: i2s_shared
    i2s_din_pin: GPIO16
    pdm: false
    adc:
      i2c_id: bus_a
      model: es7210
      address: 0x40
    sample_rate: 16000
    bits_per_sample: 16bit
    fixed_settings: true

media_player:
  - platform: adf_pipeline
    id: adf_media_player
    name: s3-box_media_player
    internal: false
    keep_pipeline_alive: true
    announcement_audio:
      sample_rate: 24000
      bits_per_sample: 16
      num_channels: 1
    pipeline:
      - self
      - resampler
      - adf_i2s_out
    on_play:
      - display.page.show: replying_page
      - component.update: s3_box_lcd

microphone:
  - platform: adf_pipeline
    id: box_mic
    keep_pipeline_alive: true
    pipeline:
      - adf_i2s_in
      - resampler
      - self

###################################################################################


micro_wake_word:
  models:
    - ${micro_wake_word_model}
  on_wake_word_detected:
    - voice_assistant.start:
        wake_word: !lambda return wake_word;

voice_assistant:
  id: va
  #microphone: box_mic
  #speaker: box_speaker
  microphone: box_mic
  media_player: adf_media_player
  noise_suppression_level: 2
  auto_gain: 31dBFS
  volume_multiplier: 2.0
  #vad_threshold: 3
  on_listening:
    - lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id};
    - text_sensor.template.publish:
        id: text_request
        state: "..."
    - text_sensor.template.publish:
        id: text_response
        state: "..."
    - script.execute: draw_display
  on_stt_vad_end:
    - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id};
    - script.execute: draw_display
  on_stt_end:
    - text_sensor.template.publish:
        id: text_request
        state: !lambda return x;
    - script.execute: draw_display
  on_tts_start:
    - media_player.volume_set: # Silenciamos el altavoz interno
        id: adf_media_player
        volume: 0%
    - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; # Cambiamos al asistente de fase
    - text_sensor.template.publish:
        id: text_response
        state: !lambda return x;
    - homeassistant.service:
        service: tts.speak
        data:
          media_player_entity_id: media_player.googlehome5013
          message: !lambda 'return x;'
          entity_id: tts.google_es_es
    - script.execute: draw_display # Adaptamos la pantalla
  on_tts_end: # Ajustamos las acciones cuando el asistente deja de hablar
    - if:
        condition:
          switch.is_off: mute
        then:
          - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
        else:
          - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
    - script.execute: draw_display

#  on_tts_stream_start:
#    - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
#    - script.execute: draw_display
#  on_tts_stream_end:
#    - if:
#        condition:
#          switch.is_off: mute
#        then:
#          - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
#        else:
#          - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
#    - script.execute: draw_display
  on_end:
    - wait_until:
        not:
          voice_assistant.is_running:
    - if:
        condition:
          and:
            - switch.is_off: mute
            - lambda: return id(wake_word_engine_location).state == "On device";
            - lambda: return id(voice_assistant_phase) != ${voice_assist_timer_finished_phase_id};
        then:
          - micro_wake_word.start:
  on_error:
    - if:
        condition:
          lambda: return !id(init_in_progress);
        then:
          - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
          - script.execute: draw_display
          - delay: 1s
          - if:
              condition:
                switch.is_off: mute
              then:
                - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
              else:
                - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
          - script.execute: draw_display
  on_client_connected:
    - lambda: id(init_in_progress) = false;
    - script.execute: start_voice_assistant
    - script.execute: draw_display
  on_client_disconnected:
    - script.execute: stop_voice_assistant
    - script.execute: draw_display
  on_timer_started:
    - script.execute: draw_display
  on_timer_cancelled:
    - script.execute: draw_display
  on_timer_updated:
    - script.execute: draw_display
  on_timer_tick:
    - script.execute: draw_display
  on_timer_finished:
    - script.execute: stop_voice_assistant
    - lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id};
    - switch.turn_on: timer_ringing
    - script.execute: draw_display
    - wait_until:
        not:
          microphone.is_capturing:
    - while:
        condition:
          switch.is_on: timer_ringing
        then:
#          - lambda: id(box_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file))); Función no soportada por media_player
          - delay: 1s
    - wait_until:
        not:
          media_player.is_playing: # Cambiamos 'speaker.is_playing' por 'media_player.is_playing'
    - switch.turn_off: timer_ringing
    - script.execute: start_voice_assistant
    - script.execute: draw_display

script:
  - id: draw_display
    then:
      - if:
          condition:
            lambda: return !id(init_in_progress);
          then:
            - if:
                condition:
                  wifi.connected:
                then:
                  - if:
                      condition:
                        api.connected:
                      then:
                        - lambda: |
                            switch(id(voice_assistant_phase)) {
                              case ${voice_assist_listening_phase_id}:
                                id(s3_box_lcd).show_page(listening_page);
                                id(s3_box_lcd).update();
                                break;
                              case ${voice_assist_thinking_phase_id}:
                                id(s3_box_lcd).show_page(thinking_page);
                                id(s3_box_lcd).update();
                                break;
                              case ${voice_assist_replying_phase_id}:
                                id(s3_box_lcd).show_page(replying_page);
                                id(s3_box_lcd).update();
                                break;
                              case ${voice_assist_error_phase_id}:
                                id(s3_box_lcd).show_page(error_page);
                                id(s3_box_lcd).update();
                                break;
                              case ${voice_assist_muted_phase_id}:
                                id(s3_box_lcd).show_page(muted_page);
                                id(s3_box_lcd).update();
                                break;
                              case ${voice_assist_not_ready_phase_id}:
                                id(s3_box_lcd).show_page(no_ha_page);
                                id(s3_box_lcd).update();
                                break;
                              case ${voice_assist_timer_finished_phase_id}:
                                id(s3_box_lcd).show_page(timer_finished_page);
                                id(s3_box_lcd).update();
                                break;
                              default:
                                id(s3_box_lcd).show_page(idle_page);
                                id(s3_box_lcd).update();
                            }
                      else:
                        - display.page.show: no_ha_page
                        - component.update: s3_box_lcd
                else:
                  - display.page.show: no_wifi_page
                  - component.update: s3_box_lcd
          else:
            - display.page.show: initializing_page
            - component.update: s3_box_lcd

  - id: fetch_first_active_timer
    then:
      - lambda: |
          const auto timers = id(va).get_timers();
          auto output_timer = timers.begin()->second;
          for (auto &iterable_timer : timers) {
            if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) {
              output_timer = iterable_timer.second;
            }
          }
          id(global_first_active_timer) = output_timer;
  - id: check_if_timers_active
    then:
      - lambda: |
          const auto timers = id(va).get_timers();
          bool output = false;
          if (timers.size() > 0) {
            for (auto &iterable_timer : timers) {
              if(iterable_timer.second.is_active) {
                output = true;
              }
            }
          }
          id(global_is_timer_active) = output;
  - id: fetch_first_timer
    then:
      - lambda: |
          const auto timers = id(va).get_timers();
          auto output_timer = timers.begin()->second;
          for (auto &iterable_timer : timers) {
            if (iterable_timer.second.seconds_left <= output_timer.seconds_left) {
              output_timer = iterable_timer.second;
            }
          }
          id(global_first_timer) = output_timer;
  - id: check_if_timers
    then:
      - lambda: |
          const auto timers = id(va).get_timers();
          bool output = false;
          if (timers.size() > 0) {
            output = true;
          }
          id(global_is_timer) = output;

  - id: draw_timer_timeline
    then:
      - lambda: |
          id(check_if_timers_active).execute();
          id(check_if_timers).execute();
          if (id(global_is_timer_active)){
            id(fetch_first_active_timer).execute();
            int active_pixels = round( 320 * id(global_first_active_timer).seconds_left / max(id(global_first_active_timer).total_seconds , static_cast<uint32_t>(1)) );
            if (active_pixels > 0){
              id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE );
              id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(active_timer_color) );
            }
          } else if (id(global_is_timer)){
            id(fetch_first_timer).execute();
            int active_pixels = round( 320 * id(global_first_timer).seconds_left / max(id(global_first_timer).total_seconds , static_cast<uint32_t>(1)));
            if (active_pixels > 0){
              id(s3_box_lcd).filled_rectangle(0 , 225 , 320 , 15 , Color::WHITE );
              id(s3_box_lcd).filled_rectangle(0 , 226 , active_pixels , 13 , id(paused_timer_color) );
            }
          }
  - id: draw_active_timer_widget
    then:
      - lambda: |
          id(check_if_timers_active).execute();
          if (id(global_is_timer_active)){
            id(s3_box_lcd).filled_rectangle(80 , 40 , 160 , 50 , Color::WHITE );
            id(s3_box_lcd).rectangle(80 , 40 , 160 , 50 , Color::BLACK );

            id(fetch_first_active_timer).execute();
            int hours_left = floor(id(global_first_active_timer).seconds_left / 3600);
            int minutes_left = floor((id(global_first_active_timer).seconds_left - hours_left * 3600) / 60);
            int seconds_left = id(global_first_active_timer).seconds_left - hours_left * 3600 - minutes_left * 60 ;
            auto display_hours = (hours_left < 10 ? "0" : "") + std::to_string(hours_left);
            auto display_minute = (minutes_left < 10 ? "0" : "") + std::to_string(minutes_left);
            auto display_seconds = (seconds_left  < 10 ? "0" : "") + std::to_string(seconds_left) ;

            std::string display_string = "";
            if (hours_left > 0) {
              display_string = display_hours + ":" + display_minute;
            } else {
              display_string = display_minute + ":" + display_seconds;
            }
            id(s3_box_lcd).printf(120, 47, id(font_timer), Color::BLACK, "%s", display_string.c_str());
          }

  - id: start_voice_assistant
    then:
      - if:
          condition:
            switch.is_off: mute
          then:
            - if:
                condition:
                  lambda: return id(wake_word_engine_location).state == "In Home Assistant";
                then:
                  - lambda: id(va).set_use_wake_word(true);
                  - voice_assistant.start_continuous:
            - if:
                condition:
                  lambda: return id(wake_word_engine_location).state == "On device";
                then:
                  - lambda: id(va).set_use_wake_word(false);
                  - micro_wake_word.start
            - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
          else:
            - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};

  - id: stop_voice_assistant
    then:
      - if:
          condition:
            lambda: return id(wake_word_engine_location).state == "In Home Assistant";
          then:
            - lambda: id(va).set_use_wake_word(false);
            - voice_assistant.stop:
      - if:
          condition:
            lambda: return id(wake_word_engine_location).state == "On device";
          then:
            - voice_assistant.stop:
            - micro_wake_word.stop:
      - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id};

switch:
  - platform: template
    name: Mute
    id: mute
    icon: "mdi:microphone-off"
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    entity_category: config
    on_turn_off:
      - if:
          condition:
            lambda: return !id(init_in_progress);
          then:
            - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
            - if:
                condition:
                  not:
                    - voice_assistant.is_running
                then:
                  - if:
                      condition:
                        lambda: return id(wake_word_engine_location).state == "In Home Assistant";
                      then:
                        - lambda: id(va).set_use_wake_word(true);
                        - voice_assistant.start_continuous
                  - if:
                      condition:
                        lambda: return id(wake_word_engine_location).state == "On device";
                      then:
                        - lambda: id(va).set_use_wake_word(false);
                        - micro_wake_word.start
            - script.execute: draw_display
    on_turn_on:
      - if:
          condition:
            lambda: return !id(init_in_progress);
          then:
            - lambda: id(va).set_use_wake_word(false);
            - voice_assistant.stop
            - micro_wake_word.stop
            - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
            - script.execute: draw_display

  - platform: template
    id: timer_ringing
    optimistic: true
    internal: true
    restore_mode: ALWAYS_OFF
    on_turn_on:
      - delay: 15min
      - switch.turn_off: timer_ringing

select:
  - platform: template
    entity_category: config
    name: Wake word engine location
    id: wake_word_engine_location
    icon: "mdi:account-voice"
    optimistic: true
    restore_value: true
    options:
      - In Home Assistant
      - On device
    initial_option: On device
    on_value:
      - if:
          condition:
            lambda: return !id(init_in_progress);
          then:
            - wait_until:
                lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
            - if:
                condition:
                  lambda: return x == "In Home Assistant";
                then:
                  - micro_wake_word.stop
                  - delay: 500ms
                  - if:
                      condition:
                        switch.is_off: mute
                      then:
                        - lambda: id(va).set_use_wake_word(true);
                        - voice_assistant.start_continuous:
            - if:
                condition:
                  lambda: return x == "On device";
                then:
                  - lambda: id(va).set_use_wake_word(false);
                  - voice_assistant.stop
                  - delay: 500ms
                  - if:
                      condition:
                        switch.is_off: mute
                      then:
                        - micro_wake_word.start

globals:
  - id: init_in_progress
    type: bool
    restore_value: false
    initial_value: "true"
  - id: voice_assistant_phase
    type: int
    restore_value: false
    initial_value: ${voice_assist_not_ready_phase_id}
  - id: global_first_active_timer
    type: voice_assistant::Timer
    restore_value: false
  - id: global_is_timer_active
    type: bool
    restore_value: false
  - id: global_first_timer
    type: voice_assistant::Timer
    restore_value: false
  - id: global_is_timer
    type: bool
    restore_value: false

image:
  - file: ${error_illustration_file}
    id: casita_error
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: ${idle_illustration_file}
    id: casita_idle
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: ${listening_illustration_file}
    id: casita_listening
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: ${thinking_illustration_file}
    id: casita_thinking
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: ${replying_illustration_file}
    id: casita_replying
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: ${timer_finished_illustration_file}
    id: casita_timer_finished
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: ${loading_illustration_file}
    id: casita_initializing
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png
    id: error_no_wifi
    resize: 320x240
    type: RGB24
    use_transparency: true
  - file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png
    id: error_no_ha
    resize: 320x240
    type: RGB24
    use_transparency: true

font:
  - file:
      type: gfonts
      family: Figtree
      weight: 300
      italic: true
    glyphs: ${allowed_characters}
    id: font_request
    size: 15
  - file:
      type: gfonts
      family: Figtree
      weight: 300
    glyphs: ${allowed_characters}
    id: font_response
    size: 15
  - file:
      type: gfonts
      family: Figtree
      weight: 300
    glyphs: ${allowed_characters}
    id: font_timer
    size: 30

text_sensor:
  - id: text_request
    platform: template
    on_value:
      lambda: |-
        if(id(text_request).state.length()>32) {
          std::string name = id(text_request).state.c_str();
          std::string truncated = esphome::str_truncate(name.c_str(),31);
          id(text_request).state = (truncated+"...").c_str();
        }

  - id: text_response
    platform: template
    on_value:
      lambda: |-
        if(id(text_response).state.length()>32) {
          std::string name = id(text_response).state.c_str();
          std::string truncated = esphome::str_truncate(name.c_str(),31);
          id(text_response).state = (truncated+"...").c_str();
        }

color:
  - id: idle_color
    hex: ${idle_illustration_background_color}
  - id: listening_color
    hex: ${listening_illustration_background_color}
  - id: thinking_color
    hex: ${thinking_illustration_background_color}
  - id: replying_color
    hex: ${replying_illustration_background_color}
  - id: loading_color
    hex: ${loading_illustration_background_color}
  - id: error_color
    hex: ${error_illustration_background_color}
  - id: active_timer_color
    hex: "26ed3a"
  - id: paused_timer_color
    hex: "3b89e3"

file:
  - id: timer_finished_wave_file
    file: https://github.com/esphome/wake-word-voice-assistants/raw/main/sounds/timer_finished.wav

spi:
  - id: spi_bus
    clk_pin: 7
    mosi_pin: 6

display:
  - platform: ili9xxx
    id: s3_box_lcd
    model: S3BOX
    data_rate: 40MHz
    cs_pin: 5
    dc_pin: 4
    reset_pin:
      number: 48
      inverted: true
    update_interval: never
    pages:
      - id: idle_page
        lambda: |-
          it.fill(id(idle_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER);
          id(draw_timer_timeline).execute();
          id(draw_active_timer_widget).execute();
      - id: listening_page
        lambda: |-
          it.fill(id(listening_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER);
          id(draw_timer_timeline).execute();
      - id: thinking_page
        lambda: |-
          it.fill(id(thinking_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER);
          it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE );
          it.rectangle(20 , 20 , 280 , 30 , Color::BLACK );
          it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str());

          id(draw_timer_timeline).execute();
      - id: replying_page
        lambda: |-
          it.fill(id(replying_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER);
          it.filled_rectangle(20 , 20 , 280 , 30 , Color::WHITE );
          it.rectangle(20 , 20 , 280 , 30 , Color::BLACK );
          it.filled_rectangle(20 , 190 , 280 , 30 , Color::WHITE );
          it.rectangle(20 , 190 , 280 , 30 , Color::BLACK );
          it.printf(30, 25, id(font_request), Color::BLACK, "%s", id(text_request).state.c_str());
          it.printf(30, 195, id(font_response), Color::BLACK, "%s", id(text_response).state.c_str());
          id(draw_timer_timeline).execute();
      - id: timer_finished_page
        lambda: |-
          it.fill(id(idle_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_timer_finished), ImageAlign::CENTER);
      - id: error_page
        lambda: |-
          it.fill(id(error_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER);
      - id: no_ha_page
        lambda: |-
          it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER);
      - id: no_wifi_page
        lambda: |-
          it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER);
      - id: initializing_page
        lambda: |-
          it.fill(id(loading_color));
          it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER);
      - id: muted_page
        lambda: |-
          it.fill(Color::BLACK);
          id(draw_timer_timeline).execute();
          id(draw_active_timer_widget).execute();
⚠️ Si vas a reemplazar tu código con este, no olvides reemplazar las credenciales de tu dispositivo en el código para que pueda conectarse a Home Assistant!

¿Dudas?¿necesitas ayuda? entra aquí
Y si te ha gustado, compártelo! 🙂
Contenido exclusivo de Aguacatec
Send this to a friend