🎡 Song Quiz Blueprint – A Music Trivia Game for Home Assistant Voice!

Hey everyone!

I recently transitioned from Amazon Echo devices to Home Assistant Voice, and one of the things I missed the most was playing Song Quiz. So, I decided to recreate it in Home Assistant using Music Assistant and conversation voice commands! :microphone::notes:

This blueprint allows you to play a music trivia game with up to four players. The game plays songs from a selected era, and players have to guess the song and artist to earn points. After five rounds, the player with the highest score wins!

For now you need to use your wake word, after every questions the voice satellite asks. I want to change that in the future when assist_satellite.start_conversation becomes available.

You can change the Spotify playlist that it will use for each era or just go with my defaults.

Update: Added an LLM Version, which uses a conversation agent to determine if the answers are correct and generates an end message. Everything else works the same.

:hammer_and_wrench: Setup Instructions

Requirements:

  • Music Assistant integration installed and configured
  • A working Home Assistant Voice setup
  • Four counters to track player scores:
    • counter.song_quiz_player_1
    • counter.song_quiz_player_2
    • counter.song_quiz_player_3
    • counter.song_quiz_player_4

How to Play

  1. Say β€œStart Song Quiz” to begin.
  2. Home Assistant will ask how many players are playing (1 player, 2 players,…)
  3. Choose a music era: 90s, 2000s, 2010s, or Now.
  4. Each player listens to a song snippet and tries to guess the song and artist.
  5. Players earn 25 points for guessing both correctly, 10 points for getting one right.
  6. After 5 rounds, the game announces the winner!

Voice Commands:

  • :green_circle: β€œStart Song Quiz” – Begins a new game
  • :red_circle: β€œStop Song Quiz” – Ends the game

:link: Download the Blueprint

Local Version:
Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

LLM Version:
Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

Paste the YAML into Automations > Blueprints, or grab it from my GitHub:
:point_right: GitHub Link

I’d love to hear feedback from the community! Let me know if you try it out or if you have any feature suggestions. :rocket::notes:

blueprint:
  name: Song Quiz - Music trivia for Home Assistant - fully local
  description: >-
    A voice-controlled music trivia game using Home Assistant Voice and Music Assistant. <br>
    Players guess songs based on a selected era, earning points for correct answers. <br>
    The game lasts 5 rounds, and the player with the highest score wins.<br>
    <br>
    For now you need to use your wake word, after every questions the voice satellite asks. <br>
    I want to change that in the future when assist_satellite.start_conversation becomes available.<br>
    <br>
    You can change the Spotify playlist that it will use for each era or just go with my defaults.<br>
    <br>
    πŸ› οΈ **Setup Instructions**<br>
    <br>
    **Requirements:**<br>
    <br>
    1. **Music Assistant** integration installed and configured <br>
    2. A working **Home Assistant Voice** setup<br>
    3. **Four counters** to track player scores:
      * `counter.song_quiz_player_1`
      * `counter.song_quiz_player_2`
      * `counter.song_quiz_player_3`
      * `counter.song_quiz_player_4`
    <br>
    **How to Play**<br>
    <br>
    1. Say **"Start Song Quiz"** to begin.<br>
    2. Home Assistant will ask how many players are playing (1 player, 2 players,...)<br>
    3. Choose a music era: **90s, 2000s, 2010s, or Now**.<br>
    4. Each player listens to a song snippet and tries to guess the **song and artist**.<br>
    5. Players earn **25 points** for guessing both correctly, **10 points** for getting one right.<br>
    6. After 5 rounds, the game announces the winner!<br>
    <br>
    **Voice Commands:**<br>
    <br>
    🟒 **"Start Song Quiz"** – Begins a new game<br>
    πŸ”΄ **"Stop Song Quiz"** – Ends the game<br>
    <br>
    **Have fun playing!** <br>
  domain: automation
  input:
    trigger_settings:
      name: Trigger settings
      icon: mdi:chat
      description:
        Settings for the trigger, you can adjust them, or translate them
        to your own language
      input:
        start_trigger:
          name: "Set Start Sentences"
          description: "Customize the voice commands for starting song quiz."
          selector:
            text:
              multiline: false
              multiple: true
          default:
            - song quiz
            - start song quiz
        stop_trigger:
          name: "Set Stop Sentences"
          description: "Customize the voice commands for stopping song quiz."
          selector:
            text:
              multiline: false
              multiple: true
          default:
            - stop song quiz
    game_rounds:
      name: Game Rounds
      description: How many rounds a game has. 
      default: 5
    default_volume:
      name: Default Volume
      description: Default volume for playing the music.
      default: 0.6
    spotify_playlist:
      name: Spotify Playlist Settings
      icon: mdi:spotify
      description:
        Set the spotify playlist for the different eras. 
      input:
        spotify_uris_90s:
          name: Spotify URI for 90s
          description: Playlist URI for the 90s era.
          default: "spotify:playlist:37i9dQZF1DXbTxeAdrVG2l"
        spotify_uris_2000s:
          name: Spotify URI for 2000s
          description: Playlist URI for the 2000s era.
          default: "spotify:playlist:37i9dQZF1DX4o1oenSJRJd"
        spotify_uris_2010s:
          name: Spotify URI for 2010s
          description: Playlist URI for the 2010s era.
          default: "spotify:playlist:37i9dQZF1DX4UtSsGT1Sbe"
        spotify_uris_now:
          name: Spotify URI for Today
          description: Playlist URI for current hits.
          default: "spotify:playlist:37i9dQZEVXbMDoHDwVN2tF"
        spotify_uris_default:
          name: Spotify URI for fallback
          description: Playlist URI as a fallback if no era is set.
          default: "spotify:playlist:37i9dQZF1DX4UtSsGT1Sbe"

trigger:
  - command: !input start_trigger
    trigger: conversation
  - trigger: conversation
    command: !input stop_trigger
    id: stop

actions:
  - variables:
      assist_area: "{{ trigger.device_id | device_attr('area_id') }}"
      media_player_temp: >-
        {% set players = area_entities(assist_area) | select('match', 'media_player\\..*') | list %}
        {% set valid_players = players | select('in', integration_entities('music_assistant')) | list %}
        {{ valid_players | first if valid_players else none }}
      media_player: "{{ media_player_temp if media_player_temp else 'media_player.' ~ trigger.device_id.split('.')[1] }}"
  - choose:
      - conditions:
          - condition: trigger
            id:
              - stop
        sequence:
          - action: media_player.media_pause
            metadata: {}
            data: {}
            target:
              entity_id: "{{ media_player }}"
          - set_conversation_response: Song quiz stopped. Thanks for playing!
    default:
      - if:
          - condition: template
            value_template: "{{ is_state(media_player,'playing') }}"
        then:
          - action: media_player.media_pause
            data: {}
            target:
              entity_id: "{{ media_player }}"
      - action: counter.reset
        target:
          entity_id:
            - counter.song_quiz_player_1
            - counter.song_quiz_player_2
            - counter.song_quiz_player_3
            - counter.song_quiz_player_4
        data: {}
      - target:
          device_id: "{{ trigger.device_id }}"
        data:
          message: >-
            Welcome to Song Quiz! How many players are playing? Names are
            optional.
        action: assist_satellite.announce
      - wait_for_trigger:
          - trigger: conversation
            command:
              - "{number} players"
              - "{number} player"
              - "{number} players {names}"
        continue_on_timeout: false
        timeout:
          hours: 0
          minutes: 0
          seconds: 30
          milliseconds: 0
      - variables:
          num_players: >-
            {% set mapping = {'One': 1, 'Two': 2, 'Three': 3, 'Four': 4} %} {{
            mapping.get(wait.trigger.slots.number, 1) }}
          names_specified: "{{ 'names' in wait.trigger.slots }}"
          player_names: |-
            {% if names_specified %}
              {{ wait.trigger.slots.names.replace(' and ', ' ').split(' ')[:num_players] }}
            {% else %}
              {{ ['Player 1', 'Player 2', 'Player 3', 'Player 4'][:num_players] }}
            {% endif %}
      - target:
          device_id: "{{ trigger.device_id }}"
        data:
          message: >-
            {% if names_specified %}Welcome, {{ player_names | join(', ') }}!{%
            else %}Great, {{ num_players }} Players! {% endif %}
        action: assist_satellite.announce
      - repeat:
          sequence:
            - target:
                device_id: "{{ trigger.device_id }}"
              data:
                message: >-
                  Choose a music era 90s, 2000s, 2010s or today.
              action: assist_satellite.announce
            - wait_for_trigger:
                - trigger: conversation
                  command:
                    - "{time}"
              continue_on_timeout: false
              timeout:
                hours: 0
                minutes: 0
                seconds: 30
                milliseconds: 0
            - variables:
                era: "{{ wait.trigger.slots.time | lower }}"
                playlist_uri: >-
                  {% if era == '90s' %} !input spotify_uris_90s
                  {% elif era == '2000s' %} !input spotify_uris_2000s
                  {% elif era == '2010s' %} !input spotify_uris_2010s
                  {% elif era == 'today' %} !input spotify_uris_now
                  {% else %} !input spotify_uris_default {% endif %}
            - if:
                - condition: template
                  value_template: '{{ era in ["90s", "2000s", "2010s", "today"] }}'
              then:
                - action: music_assistant.play_media
                  data:
                    media_type: playlist
                    media_id: "{{ playlist_uri | string }}"
                  target:
                    entity_id: "{{ media_player }}"
                - delay:
                    hours: 0
                    minutes: 0
                    seconds: 5
                    milliseconds: 0
                - action: media_player.shuffle_set
                  data:
                    shuffle: true
                  target:
                    entity_id: "{{ media_player }}"
                - delay:
                    hours: 0
                    minutes: 0
                    seconds: 2
                    milliseconds: 0
                - data:
                    entity_id: "{{ media_player }}"
                    volume_level: 0.2
                  action: media_player.volume_set
          until:
            - condition: template
              value_template: "{{ is_state(media_player,'playing') }}"
      - variables:
          round: 0
      - repeat:
          count: !input game_rounds
          sequence:
            - variables:
                round: "{{ repeat.index }}"
            - target:
                device_id: "{{ trigger.device_id }}"
              data:
                message: Round {{ round }} begins!
              action: assist_satellite.announce
            - repeat:
                count: "{{ num_players }}"
                sequence:
                  - variables:
                      player: "{{ repeat.index }}"
                      default_volume: !input default_volume
                      current_volume: >-
                        {{ current_volume if current_volume else default_volume
                        }}
                  - action: media_player.media_next_track
                    data: {}
                    target:
                      entity_id: "{{ media_player }}"
                  - target:
                      device_id: "{{ trigger.device_id }}"
                    data:
                      message: "{{ player_names[player - 1] }}, listen to your song!"
                    action: assist_satellite.announce
                  - data:
                      entity_id: "{{ media_player }}"
                      volume_level: "{{ current_volume | float }}"
                    action: media_player.volume_set
                  - delay: "00:00:30"
                  - variables:
                      current_volume: "{{ state_attr(media_player, 'volume_level') }}"
                  - data:
                      entity_id: "{{ media_player }}"
                      volume_level: 0.2
                    action: media_player.volume_set
                  - target:
                      device_id: "{{ trigger.device_id }}"
                    data:
                      message: "{{ player_names[player - 1] }}, what is your guess?"
                    action: assist_satellite.announce
                  - wait_for_trigger:
                      - trigger: conversation
                        command:
                          - "{guess}"
                    continue_on_timeout: true
                    timeout:
                      hours: 0
                      minutes: 0
                      seconds: 30
                      milliseconds: 0
                  - variables:
                      answer: "{{ wait.trigger.slots.guess | default('User gave no answer') | lower }}"
                      correct_song: >-
                        {% set song = state_attr(media_player, 'media_title') |
                        lower %} {% if song is string %}
                          {% set song = song | regex_replace(find='\[.*?\]|\(.*?\)', replace='') %} {# Remove anything in brackets #}
                          {% set song = song | regex_replace(find='[0-9!?.,]', replace='') %} {# Remove numbers, exclamation marks, and punctuation #}
                          {{ song | trim }}
                        {% else %}
                          ""
                        {% endif %}
                      correct_artist: >-
                        {% set artist = state_attr(media_player, 'media_artist')
                        | lower %} {% if artist is string %}
                          {% set artist = artist.split('/')[0] %}  {# Take only the first part if there's a '/' #}
                          {% set artist = artist | regex_replace(find='[0-9&.-]', replace='', ignorecase=True) %}  {# Remove numbers and '&' #}
                          {{ artist | trim }}
                        {% else %}
                          {% if artist is iterable %}
                            {{ artist | join(' / ') | regex_replace(find='[0-9&.-]', replace='', ignorecase=True) | trim }}
                          {% else %}
                            ""
                          {% endif %}
                        {% endif %}
                      counter_entity: |-
                        {% if player == 1 %}
                          counter.song_quiz_player_1
                        {% elif player == 2 %}
                          counter.song_quiz_player_2
                        {% elif player == 3 %}
                          counter.song_quiz_player_3
                        {% else %}
                          counter.song_quiz_player_4
                        {% endif %}
                  - choose:
                      - conditions:
                          - condition: template
                            value_template: >-
                              {{ correct_song in answer | lower and
                              correct_artist in answer | lower }}
                        sequence:
                          - action: counter.set_value
                            data:
                              value: >-
                                {{ states(counter_entity) |
                                int + 25 }}
                            target:
                              entity_id: "{{ counter_entity }}"
                          - target:
                              device_id: "{{ trigger.device_id }}"
                            data:
                              message: >-
                                Bazinga! {{ player_names[player - 1] }} now has {{
                                states(counter_entity) }}
                                points.
                            action: assist_satellite.announce
                      - conditions:
                          - condition: template
                            value_template: >-
                              {{ correct_song in answer | lower or
                              correct_artist in answer | lower }}
                        sequence:
                          - action: counter.set_value
                            data:
                              value: >-
                                {{ states(counter_entity) |
                                int + 10 }}
                            target:
                              entity_id: "{{ counter_entity }}"
                          - target:
                              device_id: "{{ trigger.device_id }}"
                            data:
                              message: >-
                                Great Job! {{ player_names[player - 1] }} now has {{
                                states(counter_entity) }}
                                points.
                            action: assist_satellite.announce
                    default:
                      - target:
                          device_id: "{{ trigger.device_id }}"
                        data:
                          message: >-
                            Sorry thats incorrect! {{ player_names[player - 1] }} now
                            has {{ states(counter_entity) }}
                            points.
                        action: assist_satellite.announce
            - if:
                - condition: template
                  value_template: "{{ round != !input game_rounds }}"
              then:
                - target:
                    device_id: "{{ trigger.device_id }}"
                  data:
                    message: >-
                      End of round {{ round }}! Here are the standings: {{
                      player_names[0] }}: {{ states("counter.song_quiz_player_1") }}
                      points, {% if num_players > 1 %} {{ player_names[1] }}: {{
                      states("counter.song_quiz_player_2") }} points, {% endif %} {%
                      if num_players > 2 %} {{ player_names[2] }}: {{
                      states("counter.song_quiz_player_3") }} points, {% endif %} {%
                      if num_players > 3 %} {{ player_names[4] }}: {{
                      states("counter.song_quiz_player_4") }} points. {% endif %}
                  action: assist_satellite.announce
      - target:
          device_id: "{{ trigger.device_id }}"
        data:
          message: "Game over! Let's see the final scores:"
        action: assist_satellite.announce
      - repeat:
          count: "{{ num_players }}"
          sequence:
            - variables:
                player: "{{ repeat.index }}"
            - target:
                device_id: "{{ trigger.device_id }}"
              data:
                message: >-
                  {{ player_names[player - 1] }} scored {{
                  states("counter.song_quiz_player_1") if player == 1 else
                  states("counter.song_quiz_player_2") if player == 2 else
                  states("counter.song_quiz_player_3") if player == 3 else
                  states("counter.song_quiz_player_4") }} points.
              action: assist_satellite.announce
      - variables:
          scores: |-
            {% set score_list = [
              states("counter.song_quiz_player_1") | int,
              states("counter.song_quiz_player_2") | int,
              states("counter.song_quiz_player_3") | int,
              states("counter.song_quiz_player_4") | int
            ] %} {{ score_list[:num_players] }}
          highest_score: "{{ scores | max }}"
          winners: |-
            {% if highest_score %}
              {% set player_ids = [1, 2, 3, 4][:num_players] %}
              {% set ns = namespace(winners=[]) %}
              {% for i in range(num_players) %}
                {% if scores[i] == highest_score %}
                  {% set ns.winners = ns.winners + [player_ids[i]] %}
                {% endif %}
              {% endfor %}
              {{ ns.winners }}
            {% else %}
              []
            {% endif %}
      - target:
          device_id: "{{ trigger.device_id }}"
        data:
          message: |-
            {% if not highest_score %}
              No winner this time! Better luck next time!
            {% elif winners | length == 1 %}
              The winner is Player {{ winners[0] }}! Congratulations!
            {% else %}
              It's a tie between Players {{ winners | join(' and ') }}! Well played!
            {% endif %}
        action: assist_satellite.announce
      - action: media_player.media_pause
        metadata: {}
        data: {}
        target:
          entity_id: "{{ media_player }}"
mode: restart
7 Likes

I did a few edits. I added an input for the default music volume and the number of game rounds, so it is a bit more customizable.

I am also working on an LLM-enhanced version. It uses an LLM to determine if the answers is correct, which gives way better results, and generates a custom end message to announce the winner. If there is interest for it I can share it as well.

1 Like

I did another upgrade, it is now possible to set names for the players. This is optional though.

1 Like

This looks fun!

1 Like

did some minor changes, for example if there is no answer of the user.

This looks interesting, nice work but does it require a paid Spotify account?

thank you. yes music assistant requires a premium Spotify account.

Added an LLM Version of the Blueprint. It works basically the same, but uses an LLM to determine correct answers and generates an end message for each game.