Salta al contenuto principale
KidJuke: La Radio Magica dei Bambini
  1. Articles/

KidJuke: La Radio Magica dei Bambini

Indice dei contenuti

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-C3RC522Pulsanti
1Volume Su
3Volume Giù
4SCK
5MISO
6MOSI
7SDA
10Play/Pause
20Prossima Traccia
21RST

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: 5ms

Il 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.

ViteLunghezzaQuantità
M316mm4
M2.55mm1

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:

CampoValoriOsservazioni
Lettore MusicaleEntità MAScegli il media_player di Music Assistant da controllare.
Interruttore di BloccoEntità input_booleanObbligatorio. Crea un helper per bloccare i comandi (es. “ora di dormire”).
Sensore RFIDEntità testoRiceve l’ID della carta o il testo da associare alla musica.
Pulsante Play/PauseEntitàDeve inviare un segnale ON al premere. In ESPHome c’è già un filtro anti-bouncing.
Pulsante Next SongEntità
Pulsante Volume UPEntità
Pulsante Volume DOWNEntità
Volume Minimo0.0 - 1.0Volume minimo (0 = 0%, 1 = 100%).
Volume Massimo0.0 - 1.0Volume massimo consentito dai pulsanti.
Step Volume0.01 - 0.2Di quanto varia il volume ad ogni click.
Ritardo Comandi0 - 3 secTempo di attesa dopo un comando prima di accettarne un altro.
Tipo Media Defaultplaylist, album, track, artist, radioTipo di ricerca se non specificato nella carta.
Azione Coda Defaultplay, next, add, replaceSe la musica sostituisce la coda o si aggiunge.
Shuffle Defaulttrue, falseRiproduzione casuale di default.
Mappatura Carte RFIDID:query:tipo:coda:shuffleDue formati possibili: breve (usa default) o avanzato (specifica tutto per ogni carta). Ogni riga inizia con “-”.

Il codice del Blueprint
#

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

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!

Articoli correlati