KidJuke the alternative to Faba/Toniebox/Yoto in Home Assistant

Hi everyone!

Meet KidJuke, the fun and open alternative to Faba, Toniebox, or Yoto for your Home Assistant setup! Here’s the magic: ESPHome handles the hardware, a Home Assistant blueprint powers the automation, and Music Assistant takes care of the tunes.

With KidJuke, kids can easily control their music—play, pause, skip tracks, and adjust volume—plus pick what to listen to (playlists, albums, radio, artists) using RFID cards. I’ve detailed the build process in this post.

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

Features

  • Map RFID cards to specific music
  • Set minimum and maximum allowed volume
  • Define volume step increments
  • Set a default behavior for simple mappings
  • Add an input_boolean to completely lock commands (perfect for bedtime!)
  • Choose whether to enable random playback by default
  • Set a delay between button commands
  • Select which media player to control

Requirements

  • Hardware: A KidJuke (or compatible switches work too!)
  • Integration: Music Assistant
  • Other entities: You’ll need an input_boolean not provided by ESPHome; a helper like a complete device lock works perfectly.

Instructions

  1. Install a KidJuke in ESPHome
  2. Integrate the KidJuke into Home Assistant
  3. Create an input_boolean helper for the lock function
  4. Confirm that Music Assistant is active
  5. Install the blueprint and create an automation based on it
  6. Happy listening!

Code

blueprint:
  name: "KidJuke Controller (Music Assistant)"
  description: "Manages the complete KidJuke controller with buttons, RFID and Music Assistant integration."
  domain: automation
  input:
    media_player:
      name: Music Player (Music Assistant)
      description: The media_player managed by Music Assistant on which to play the music.
      selector:
        entity:
          domain: media_player
    enabled_switch:
      name: Lock Switch / Disable
      description: Switch (e.g., input_boolean) to block commands when ON (e.g., child lock).
      selector:
        entity:
          domain: input_boolean
    rfid_sensor:
      name: ESPHome RFID Sensor
      description: The ESPHome text sensor that receives the RFID card ID (keeps the last read).
      selector:
        entity:
          domain: sensor
    btn_play_pause:
      name: Play/Pause Button
      description: Entity that detects the Play/Pause button press.
      selector:
        entity:
          domain: [binary_sensor]
    btn_next:
      name: Next Song Button
      description: Entity that detects the button press for the next track.
      selector:
        entity:
          domain: [binary_sensor]
    btn_volume_up:
      name: Volume Up Button
      description: Entity that detects the button press to increase volume.
      selector:
        entity:
          domain: [binary_sensor]
    btn_volume_down:
      name: Volume Down Button
      description: Entity that detects the button press to decrease volume.
      selector:
        entity:
          domain: [binary_sensor]
    min_volume:
      name: Minimum Volume
      description: Minimum volume applied when starting playback via RFID card (from 0.0 to 1.0).
      default: 0.1
      selector:
        number:
          min: 0.0
          max: 1.0
          step: 0.05
          mode: slider
    max_volume:
      name: Maximum Volume
      description: Maximum volume allowed for the player via buttons (from 0.0 to 1.0).
      default: 0.7
      selector:
        number:
          min: 0.0
          max: 1.0
          step: 0.05
          mode: slider
    volume_step:
      name: Volume Variation Step
      description: How much the volume varies with each button press (from 0.01 to 0.2).
      default: 0.05
      selector:
        number:
          min: 0.01
          max: 0.2
          step: 0.01
          mode: slider
    command_delay:
      name: Delay after button commands
      description: Wait time (in seconds) after executing a button (excluding volume down).
      default: 0.3
      selector:
        number:
          min: 0.0
          max: 3.0
          step: 0.1
          mode: slider
    default_media_type:
      name: Default Media Type
      description: Fallback value if not specified on the card (e.g., playlist, album, track, artist).
      default: playlist
      selector:
        select:
          options:
            - playlist
            - album
            - track
            - artist
            - radio
    default_enqueue:
      name: Default Queue Action (Enqueue)
      description: Replacement (play) or queuing (add/next) if not specified on the card.
      default: play
      selector:
        select:
          options:
            - play
            - next
            - add
            - replace
    default_shuffle:
      name: Default Random Playback (Shuffle)
      description: Activate or deactivate random playback by default if not specified on the card.
      default: false
      selector:
        boolean:
    card_mappings:
      name: RFID Card Mapping (Advanced Format)
      description: >
        String format: card_ID:search_text:media_type:queue_replacement:random_playback
        (The last three fields are optional).
      selector:
        object:
      default:
        - "12-34-56-78:90s Hits:playlist:play:false"
        - "87654321:Bedtime Stories:album"

mode: queued

trigger:
  - platform: state
    entity_id: !input btn_play_pause
    id: btn_play_pause
    to: "on"
  - platform: state
    entity_id: !input btn_next
    id: btn_next
    to: "on"
  - platform: state
    entity_id: !input btn_volume_up
    id: btn_volume_up
    to: "on"
  - platform: state
    entity_id: !input btn_volume_down
    id: btn_volume_down
    to: "on"

action:
  # CHILD LOCK CONTROL: if the switch is ON, the condition fails and execution stops
  - condition: state
    entity_id: !input enabled_switch
    state: "off"

  - variables:
      media_player_entity: !input media_player
      rfid_sensor_entity: !input rfid_sensor
      min_vol: !input min_volume
      max_vol: !input max_volume
      step_val: !input volume_step
      cmd_delay: !input command_delay
      def_media_type: !input default_media_type
      def_enqueue: !input default_enqueue
      def_shuffle: !input default_shuffle
      raw_mappings: !input card_mappings
      parsed_mappings: >
        {% set valid_types = ['playlist', 'album', 'track', 'artist', 'radio'] %}
        {% set ns = namespace(result=[]) %}
        {% for item in raw_mappings %}
          {% set parts = item.split(':') %}
          {% if parts | length >= 2 %}
            {% set rfid = parts[0] | trim %}
            {% set query = parts[1] | trim %}
            
            {% set p2 = parts[2] | trim if parts | length > 2 else '' %}
            {% set p3 = parts[3] | trim if parts | length > 3 else '' %}
            {% set p4 = parts[4] | trim if parts | length > 4 else '' %}

            {% if p2 in valid_types %}
              {% set m_type = p2 %}
              {% set enq = p3 if p3 != '' else def_enqueue %}
              {% set shuf = (p4 | lower == 'true') if p4 != '' else def_shuffle %}
            {% elif p2 != '' and p2 not in valid_types %}
              {% set m_type = def_media_type %}
              {% set enq = p2 %}
              {% set shuf = (p3 | lower == 'true') if p3 != '' else def_shuffle %}
            {% else %}
              {% set m_type = def_media_type %}
              {% set enq = def_enqueue %}
              {% set shuf = def_shuffle %}
            {% endif %}

            {% set ns.result = ns.result + [{
              'rfid': rfid,
              'query': query,
              'media_type': m_type,
              'enqueue': enq,
              'shuffle': shuf
            }] %}
          {% endif %}
        {% endfor %}
        {{ ns.result }}

  - choose:
      # 1. PLAY / PAUSE MANAGEMENT (with delay applied)
      - conditions:
          - condition: trigger
            id: btn_play_pause
        sequence:
          - variables:
              player_state: "{{ states[media_player_entity].state }}"
              last_card: "{{ states[rfid_sensor_entity].state if rfid_sensor_entity != '' else '' }}"
              matched_last_item: "{{ parsed_mappings | selectattr('rfid', 'eq', last_card) | first | default(none) }}"

          - choose:
              # If music is playing, pause it
              - conditions:
                  - "{{ player_state == 'playing' }}"
                sequence:
                  - service: media_player.media_play_pause
                    target:
                      entity_id: "{{ media_player_entity }}"
              # If music is NOT playing and a valid card is stored, start playback of the card
              - conditions:
                  - "{{ last_card not in ['', 'unknown', 'unavailable'] }}"
                  - "{{ matched_last_item is not none and matched_last_item.query is defined }}"
                sequence:
                  - service: media_player.volume_set
                    target:
                      entity_id: "{{ media_player_entity }}"
                    data:
                      volume_level: "{{ min_vol }}"
                  - service: media_player.shuffle_set
                    target:
                      entity_id: "{{ media_player_entity }}"
                    data:
                      shuffle: "{{ matched_last_item.shuffle }}"
                  - service: music_assistant.play_media
                    target:
                      entity_id: "{{ media_player_entity }}"
                    data:
                      media_id: "{{ matched_last_item.query }}"
                      media_type: "{{ matched_last_item.media_type }}"
                      enqueue: "{{ matched_last_item.enqueue }}"
            default:
              # Standard play/pause fallback if not playing and no valid cards stored
              - service: media_player.media_play_pause
                target:
                  entity_id: "{{ media_player_entity }}"
          - delay: "{{ cmd_delay }}"

      # 2. NEXT SONG MANAGEMENT (with delay applied)
      - conditions:
          - condition: trigger
            id: btn_next
        sequence:
          - service: media_player.media_next_track
            target:
              entity_id: "{{ media_player_entity }}"
          - delay: "{{ cmd_delay }}"

      # 3. VOLUME UP MANAGEMENT (with delay applied)
      - conditions:
          - condition: trigger
            id: btn_volume_up
        sequence:
          - variables:
              current_vol: "{{ state_attr(media_player_entity, 'volume_level') | float(0) }}"
              new_vol: "{{ [current_vol + step_val, max_vol] | min }}"
          - service: media_player.volume_set
            target:
              entity_id: "{{ media_player_entity }}"
            data:
              volume_level: "{{ new_vol }}"
          - delay: "{{ cmd_delay }}"

      # 4. VOLUME DOWN MANAGEMENT (WITHOUT any delay, completely instantaneous)
      - conditions:
          - condition: trigger
            id: btn_volume_down
        sequence:
          - variables:
              current_vol: "{{ state_attr(media_player_entity, 'volume_level') | float(0) }}"
              new_vol: "{{ [current_vol - step_val, min_vol] | max }}"
          - service: media_player.volume_set
            target:
              entity_id: "{{ media_player_entity }}"
            data:
              volume_level: "{{ new_vol }}"