Cinque anni fa ho realizzato una scatola speciale per permettere alla mia prima bambina di gestire la musica nella sua cameretta. Con il crescere dei bambini, ho aggiornato il sistema per renderlo più robusto e, appena ho preso una stampante 3D, ho voluto riprogettare il case per renderlo ancora più semplice da costruire e renderlo disponibile a tutti.
Presento a voi KidJuke!
È un controller che usa le carte RFID per scegliere playlist, album o brani preferiti. Grazie a quattro pulsanti, i bambini possono fermare la musica, farla ripartire, passare alla traccia successiva e regolare il volume. Tutto questo gira su un ecosistema potente: Music Assistant, Home Assistant e ESPHome.
Come funziona?#
L’idea è semplice ma efficace:
- ESPHome legge i pulsanti e riconosce quando viene avvicinata una nuova carta RFID. Il sistema è intelligente: rileva il cambio di carta solo quando c’è una variazione.
- Home Assistant usa un Blueprint per collegare il controller ESPHome alle automazioni. Un helper binario permette di bloccare tutto (utile quando i bambini non si comportano o è ora di dormire).
- Music Assistant gestisce le code di riproduzione e i file multimediali.
Con le carte RFID, si può selezionare una playlist, un album o una traccia specifica su Music Assistant. Ogni carta può essere programmata per aggiungere alla coda o sostituirla completamente.
Un dettaglio importante: quando si appoggia una carta, la musica non parte subito. Serve premere il pulsante “Play”. Questa scelta evita fastidi notturni: se arriva un aggiornamento software mentre tutti dormono, la musica non si accenderà da sola nella stanza dei bambini! 😴 (Niente più bassi al volume massimo per il fattore “HAF” o “WAF”!).
Hardware con ESPHome#
Per i pulsanti ho scelto degli switch momentanei da 16mm, semplici ed economici. La lettura delle carte è affidata a un modulo RC522 (tramite SPI), tutto collegato a un ESP32-C3 versione Super Mini.
Mappa dei pin#
| ESP32-C3 | RC522 | Pulsanti |
|---|---|---|
| 1 | Volume Su | |
| 3 | Volume Giù | |
| 4 | SCK | |
| 5 | MISO | |
| 6 | MOSI | |
| 7 | SDA | |
| 10 | Play/Pause | |
| 20 | Prossima Traccia | |
| 21 | RST |
Configurazione ESPHome#
Ecco il codice per configurare il dispositivo.
# Board: ESP32-C3 Super Mini (Generic)
# Definition: definitions/boards/esp32-c3-supermini/manifest.yaml
esphome:
name: kidjuke
friendly_name: KidJuke
esp32:
variant: esp32c3
flash_size: 4MB
framework:
type: esp-idf
logger:
api:
encryption:
key: "REDACTED"
ota:
- platform: esphome
encryption:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
spi:
clk_pin: GPIO4
miso_pin: GPIO5
mosi_pin: GPIO6
# Variabile globale per memorizzare l'ultima carta letta
globals:
- id: last_card_uid
type: std::string
restore_value: false
initial_value: '""'
- id: ignore_first_tag
type: bool
restore_value: false
initial_value: 'true'
# Sensore di testo che comunica l'UID della carta a Home Assistant
text_sensor:
- platform: template
name: "Ultima Carta RFID"
id: ultima_carta_rfid
rc522_spi:
cs_pin:
number: GPIO7
reset_pin: GPIO21
on_tag:
then:
- lambda: |-
std::string current_uid = x;
// 1. La prima carta letta all'avvio del device viene completamente ignorata
if (id(ignore_first_tag)) {
ESP_LOGD("rfid", "Prima carta all'avvio ignorata: %s", current_uid.c_str());
return;
}
// 2. Invia l'UID a Home Assistant se è diverso dall'ultimo registrato
if (current_uid != id(last_card_uid)) {
id(last_card_uid) = current_uid;
ESP_LOGD("rfid", "Nuovo UID memorizzato e inviato a Home Assistant: %s", current_uid.c_str());
# Invia il nuovo UID al text_sensor di Home Assistant
id(ultima_carta_rfid).publish_state(current_uid);
} else {
ESP_LOGD("rfid", "Stessa carta rilevata, UID già memorizzato: %s", current_uid.c_str());
}
on_tag_removed:
then:
- lambda: |-
if (id(ignore_first_tag)) {
# La primissima carta all'avvio è stata rimossa: sblocchiamo il sistema
id(ignore_first_tag) = false;
ESP_LOGD("rfid", "Prima carta rimossa. Il lettore è ora pienamente attivo.");
} else {
# La carta è stata rimossa ma manteniamo l'UID salvato sul sensore e in memoria
ESP_LOGD("rfid", "Carta rimossa. L'UID salvato rimane memorizzato.");
}
binary_sensor:
- platform: gpio
name: Play - Pause
id: binary_sensor_play_pause
pin:
number: GPIO10
mode:
input: true
pullup: true
inverted: true
icon: "mdi:play-pause"
filters:
- delayed_on: 5ms
- platform: gpio
name: Next
id: binary_sensor_next_song
pin:
number: GPIO20
mode:
input: true
pullup: true
inverted: true
icon: "mdi:skip-next-outline"
filters:
- delayed_on: 5ms
- platform: gpio
name: Volume UP
id: binary_sensor_volume_up
pin:
number: GPIO1
mode:
input: true
pullup: true
inverted: true
icon: "mdi:volume-plus"
filters:
- delayed_on: 5ms
- platform: gpio
name: Volume DOWN
id: binary_sensor_volume_down
pin:
number: GPIO3
mode:
input: true
pullup: true
inverted: true
icon: "mdi:volume-minus"
filters:
- delayed_on: 5msIl case 3D#
Ho progettato il case per rendere sempre visibile la carta scelta: così i bambini possono personalizzare le proprie carte RFID! I pulsanti sono in alto: se premuti, la forza spinge verso il basso, evitando che la scatola scivoli per la stanza. La porta USB-C è sul retro per alimentare l’ESP32-C3.
Il box è diviso in due parti (coperchio e base) chiuse da viti che tengono fermo l’ESP32. Sulla base ho aggiunto strisce di silicone per evitare scivolamenti. Ho anche creato un adattatore per rendere le carte RFID in formato “carta di credito” meno fragili.
I file di stampa sono disponibili su Printables.
| Vite | Lunghezza | Quantità |
|---|---|---|
| M3 | 16mm | 4 |
| M2.5 | 5mm | 1 |
Il Blueprint Home Assistant#
Il cuore del sistema è il Blueprint. Rende la configurazione semplice e veloce, nascondendo la complessità dell’automazione. Questo blueprint è nato per KidJuke ma può gestire altri dispositivi, purché abbiate Music Assistant installato.
Lo trovate qui: Post sul Blueprint Exchange.
Parametri di configurazione#
Ecco cosa puoi personalizzare:
| Campo | Valori | Osservazioni |
|---|---|---|
| Lettore Musicale | Entità MA | Scegli il media_player di Music Assistant da controllare. |
| Interruttore di Blocco | Entità input_boolean | Obbligatorio. Crea un helper per bloccare i comandi (es. “ora di dormire”). |
| Sensore RFID | Entità testo | Riceve l’ID della carta o il testo da associare alla musica. |
| Pulsante Play/Pause | Entità | Deve inviare un segnale ON al premere. In ESPHome c’è già un filtro anti-bouncing. |
| Pulsante Next Song | Entità | |
| Pulsante Volume UP | Entità | |
| Pulsante Volume DOWN | Entità | |
| Volume Minimo | 0.0 - 1.0 | Volume minimo (0 = 0%, 1 = 100%). |
| Volume Massimo | 0.0 - 1.0 | Volume massimo consentito dai pulsanti. |
| Step Volume | 0.01 - 0.2 | Di quanto varia il volume ad ogni click. |
| Ritardo Comandi | 0 - 3 sec | Tempo di attesa dopo un comando prima di accettarne un altro. |
| Tipo Media Default | playlist, album, track, artist, radio | Tipo di ricerca se non specificato nella carta. |
| Azione Coda Default | play, next, add, replace | Se la musica sostituisce la coda o si aggiunge. |
| Shuffle Default | true, false | Riproduzione casuale di default. |
| Mappatura Carte RFID | ID:query:tipo:coda:shuffle | Due formati possibili: breve (usa default) o avanzato (specifica tutto per ogni carta). Ogni riga inizia con “-”. |
Il codice del Blueprint#
blueprint:
name: "KidJuke Controller (Music Assistant)"
description: "Gestisce il controller KidJuke completo con pulsanti, RFID e integrazione Music Assistant."
domain: automation
input:
media_player:
name: Lettore Musicale (Music Assistant)
description: Il media_player gestito da Music Assistant su cui riprodurre la musica.
selector:
entity:
domain: media_player
enabled_switch:
name: Interruttore di Blocco / Disabilitazione
description: Switch (es. input_boolean) per bloccare i comandi quando è ON (es. blocco bambini).
selector:
entity:
domain: input_boolean
rfid_sensor:
name: Sensore RFID ESPHome
description: Il sensore di testo di ESPHome che riceve l'ID della carta RFID (mantiene l'ultima letta).
selector:
entity:
domain: sensor
btn_play_pause:
name: Pulsante Play/Pause
description: Entità che rileva la pressione del pulsante Play/Pause.
selector:
entity:
domain: [binary_sensor]
btn_next:
name: Pulsante Next Song
description: Entità che rileva la pressione del pulsante per la traccia successiva.
selector:
entity:
domain: [binary_sensor]
btn_volume_up:
name: Pulsante Volume Su
description: Entità che rileva la pressione del pulsante per aumentare il volume.
selector:
entity:
domain: [binary_sensor]
btn_volume_down:
name: Pulsante Volume Giù
description: Entità che rileva la pressione del pulsante per diminuire il volume.
selector:
entity:
domain: [binary_sensor]
min_volume:
name: Volume Minimo
description: Volume minimo applicato all'avvio della riproduzione tramite carta RFID (da 0.0 a 1.0).
default: 0.1
selector:
number:
min: 0.0
max: 1.0
step: 0.05
mode: slider
max_volume:
name: Volume Massimo
description: Volume massimo consentito per il lettore tramite i pulsanti (da 0.0 a 1.0).
default: 0.7
selector:
number:
min: 0.0
max: 1.0
step: 0.05
mode: slider
volume_step:
name: Step di Variazione Volume
description: Di quanto varia il volume ad ogni pressione del pulsante (da 0.01 a 0.2).
default: 0.05
selector:
number:
min: 0.01
max: 0.2
step: 0.01
mode: slider
command_delay:
name: Ritardo dopo i comandi dei pulsanti
description: Tempo di attesa (in secondi) dopo l'esecuzione di un pulsante (escluso volume giù).
default: 0.3
selector:
number:
min: 0.0
max: 3.0
step: 0.1
mode: slider
default_media_type:
name: Tipo di Media predefinito
description: Valore di fallback se non specificato nella carta (es. playlist, album, track, artist).
default: playlist
selector:
select:
options:
- playlist
- album
- track
- artist
- radio
default_enqueue:
name: Azione coda predefinita (Enqueue)
description: Sostituzione (play) o accodamento (add/next) se non specificato nella carta.
default: play
selector:
select:
options:
- play
- next
- add
- replace
default_shuffle:
name: Riproduzione Casuale predefinita (Shuffle)
description: Attiva o disattiva la riproduzione casuale di default se non specificato nella carta.
default: false
selector:
boolean:
card_mappings:
name: Mappatura Carte RFID (Formato Avanzato)
description: >
Formato stringa: ID_carta:testo_ricerca:tipo_di_media:sostituzione_coda_riproduzione:riproduzione_causale
(Gli ultimi tre campi sono facoltativi).
selector:
object:
default:
- "12-34-56-78:Hit Anni 90:playlist:play:false"
- "87654321:Favole per bambini: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:
# CONTROLLO BLOCCHETTO / CHILD LOCK: se lo switch è ON, la condizione fallisce e interrompe l'esecuzione
- 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. GESTIONE PLAY / PAUSE (con delay applicato)
- 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:
# Se la musica è in esecuzione, mettila in pausa
- conditions:
- "{{ player_state == 'playing' }}"
sequence:
- service: media_player.media_play_pause
target:
entity_id: "{{ media_player_entity }}"
# Se la musica NON è in esecuzione e c'è una carta valida memorizzata, avvia la riproduzione della carta
- 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:
# Fallback standard play/pause se non sta suonando e non ci sono carte valide memorizzate
- service: media_player.media_play_pause
target:
entity_id: "{{ media_player_entity }}"
- delay: "{{ cmd_delay }}"
# 2. GESTIONE NEXT SONG (con delay applicato)
- conditions:
- condition: trigger
id: btn_next
sequence:
- service: media_player.media_next_track
target:
entity_id: "{{ media_player_entity }}"
- delay: "{{ cmd_delay }}"
# 3. GESTIONE VOLUME UP (con delay applicato)
- 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. GESTIONE VOLUME DOWN (SENZA alcun delay, completamente istantaneo)
- 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 }}"Spero che questo progetto ti sia utile per creare un po’ di musica nella vita dei tuoi piccoli!



