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.
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_booleanto 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_booleannot provided by ESPHome; a helper like a complete device lock works perfectly.
Instructions
- Install a KidJuke in ESPHome
- Integrate the KidJuke into Home Assistant
- Create an
input_booleanhelper for the lock function - Confirm that Music Assistant is active
- Install the blueprint and create an automation based on it
- 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 }}"
