Un piacevole accompagnamento musicale, mentre svolge i suoi compiti al servizio dell’efficienza domestica e della nostra comodità. Con questo package e la sua dashboard doniamo un po' di brio alla nostra casa con l’aiuto del nostro hub domotico.

Cosa vogliamo fare

Come abbiamo detto tante volte, una casa smart si prende cura di noi essendo in grado di riconoscere ed adattarsi silenziosamente alle nostre esigenze. Ma il benessere fisico passa senza dubbio alcuno anche da quello spirituale. In questo post andiamo perciò a realizzare un centro musicale interamente web, un player Radio e Spotify.

Nota Alcune delle funzionalità descritte richiedono un abbonamento Spotify Premium. Tuttavia il package è in grado di funzionare ugualmente anche con il profilo free, con le medesime limitazioni riscontrabili su qualsiasi altro dispositivo connesso a Spotify.

Cosa ci serve

Supponiamo di avere in casa almeno uno speaker integrato in Home Assistant: nel mio caso son o presenti tre dispsitivi Google Home Mini, indirizzabili singolarmente oppure in due gruppi (tutti o solo due di questi) definiti mediante app Google Home.

I componenti di Home Assitant necessari per quest progetto sono:

Rimandiamo alle guide ufficiali ed ai numerosi articoli presenti in rete su come predisporre tutto quanto necessario ad utilizzare i componenti sopra elencati.

La logica

Lo sforzo maggiore nella realizzazione di questo Music Player è stato quello profuso per la convivenza delle due anime Web-Radio e Spotify. Nella stessa dashboard infatti troviamo tutti i controlli necessari per gestire:

  • gli speaker, nel nostro caso 3 dispositivi e 2 gruppi
  • le sorgenti audio, ovvero stream radio e/o playlist Spotify
  • la riproduzione (anche contemporanea) dalle sorgenti

L’interazione utente è realizzata mediante alcuni input_select con i quali implementiamo le selezioni di sorgente, speaker e playlist. La variazione di queste selezioni fa scattare le automazioni per l’avvio/spegnimento della riproduzione e per il controllo degli speaker.

Una serie di sensori template ci informa in ogni momento sullo stato del player, consentendoci di condizionare l’esecuzione delle automazioni e al contempo di visualizzarne le informazioni sulla dashboard.

Gli stream audio gestibili contemporaneamente sono due: interno, sia Radio sia Spotify, mediante media_player definiti in Home Assistant, ed esterno, solo Spotify, se è attiva la riproduzione su altri dispositivi non integrati.

Nota Ricordiamo che, a differenza di Alexa e di altri dispositivi come i Sonos, l’integrazione degli speaker Google in Spotify non consente di definire in modo perenne i relativi device, ma è necessario avviare la riproduzione su uno di questi per averne visibilità. Per fare questo utilizziamo il componente Spotcast. In alternativa si può lanciare la riproduzione con un comando vocale.

Integrazioni native Google Cast e Spotify

Come anticipato, l’integrazione di questi, e degli altri, componenti necessari al progetto non è spiegata in questo post, essendo semplice e ben documentata in rete.

Quello che ci interessa qui è che, ad integrazione ultimata, se tutto saràa andato correttamente, avremo a disposizione in Home Assistant le entità:

  • media_player.tuoi_device: una entità per ciascuno dei dispotivi e dei gruppi Google Cast disponibili
  • media_player.spotify: collegato alla riproduzione su account Spotify

Scriviamo i file yaml

Vista la numerosità delle funzionalità che vogliamo implementare, il codice da scrivere è abbastanza corposo, ma con un attento utilizzo dei template (a tutti i livelli) abbiamo provato a semplificarlo. Inoltre, per comodità, lo organizziamo su più file .yaml:

  • cartella config/packages:
    • pkg_music_player: file principale del package
  • cartella config/automations:
    • auto_music_player: file con le automazioni base
    • auto_music_player_playlist: file con le automazioni per selezione stream
  • cartella config/lovelace-views:
    • view_music_player: implementazione interfaccia lovelace
    • popup_music_player: file con il contenuto dei popup

Package

Come sempre, il nostro package avrà in testa una sezione obbligatoria di personaizzazione, nella quale andiamo a dare un nome amichevole ed una icona alle entità di seguito definite, ad esempio:

homeassistant:
  customize:
    sensor.audio_speaker_attivo:
      friendly_name: Speaker Attivo   
      icon: mdi:audio-cast
    # altre entità
    ...

Come abbiamo anticipato, le interazioni utente sono realizzate mediante dei selettori, che andiamo a definire di seguito, in modo abbastanza auto-esplicativo:

  # selettore sorgente audio
  radio_spotcast_source:
    name: Radio_Spotify Source
    options:
      - Radio
      - Spotify
      - Spento
    initial: Spento
    icon: mdi:apple-airplay
  # selettore player
  radio_player:
    name: Radio Player
    options:
      - Tutta_casa
      - Diffusione
      - Camera
      ...
      - Spento
    initial: Spento
    icon: mdi:speaker    
  # selettore playlist spotify
  radio_spotcast_playlist:
    name: Spotify Playlist
    options:
      - Daily Mix 1
      - Daily Mix 2
      ...
      - Spento
    icon: mdi:spotify
  # selettore stream radio
  radio_station:
    name: Radio Station
    options:
        - Radio Deejay
        - Rai Radio 1
        - Rai Radio 2
        ...
        - Spento
    initial: Spento
    icon: mdi:radio

Sensori

Procediamo con i primi sensori template, che, rispettivamente, ci dicono in ogni momento quale speaker è in riproduzione (ovvero il suo stato assume valore playing) e quale è lo speaker selezionato dall’utente in interfaccia:

sensor:
  - platform: template
    sensors:
      audio_speaker_attivo:
        value_template: >     
          {% if (states.media_player.diffusione.state=="playing") %}
            {{'media_player.diffusione'}}            
          {% elif (states.media_player.mini_cucina.state=="playing") %}
            {{'media_player.mini_cucina'}}
          {% elif (states.media_player.mini_soggiorno.state=="playing") %}
            {{'media_player.mini_soggiorno'}}
          {% elif (states.media_player.mini_camera.state=="playing") %}
            {{'media_player.mini_camera'}}
          {% else %}
            {{'media_player.tutta_casa'}}
          {% endif %}
      audio_speaker_selezionato:
        value_template: >   
          {% if (states.input_select.radio_player.state in ["Spento", "Tutta casa"]) %}
            {{'media_player.tutta_casa'}}
          {% elif (states.input_select.radio_player.state=="Diffusione") %}
            {{'media_player.diffusione'}}                      
          {% else %}
            media_player.mini_{{states.input_select.radio_player.state.lower()}}
          {% endif %}

Ugualmente, sempre mediante la stessa piattaforma template, implementiamo dei sensori binari (on/off) che ci diano informazioni sullo stato della riproduzione:

  • audio_speaker_selezionato_muted indica se lo speaker selezionato è in muto
  • audio_riproduzione indica se c’è una riproduzione in corso sui dispositivi integrati
  • audio_spotify indica se il componente Spotify è attivo
  • audio_spotify_esterno indica se l’account spotify è in uso per una riproduzione su dispotivi esterni
  • audio_radio indica se la radio è in riproduzione
binary_sensor:
  - platform: template
    sensors:
      audio_speaker_selezionato_muted:
        value_template: >
                    {{ states[states.sensor.audio_speaker_selezionato.state].attributes.is_volume_muted }}
      audio_riproduzione: 
        value_template: >     
          {{ states.media_player.mini_cucina.state=="playing"
             or states.media_player.mini_soggiorno.state=="playing"
             or states.media_player.mini_camera.state=="playing" }}
      audio_spotify: 
        value_template: >     
          {{ states.media_player.spotify.state!="idle" }}
      audio_spotify_esterno: 
        value_template: >     
          {{ states.media_player.spotify.state!="idle" and 
            'Mini Cucina' not in states.media_player.spotify.attributes.source_list and 
            'Mini Soggiorno' not in states.media_player.spotify.attributes.source_list and 
            'Mini Camera' not in states.media_player.spotify.attributes.source_list }}          
      audio_radio: 
        value_template: >     
          {{ states.binary_sensor.audio_riproduzione.state=="on" and states.binary_sensor.audio_spotify.state=="off" }}         

Automazioni

Nella sezione automation: implementiamo tutti gli automatismi alla base del funzionamento del Music Player. Viste però le dimensioni importanti, per favorirne una gestione più comoda ed ordinata, possiamo tenere le automazioni in un file separato, da posizionare insieme a tutte le automazioni in una cartella automations creata all’occorrenza. Queste automazioni saranno poi richiamate nel configuration.yaml in questo modo:

# configuration.yaml
automation: !include_dir_merge_list automations 

Nel file auto_music_player_playlist.yaml implementiamo la selezione delle sorgenti audio (Radio/Spotify) e dei contenuti (stazione/playlist), che, mediante le rispettive automazioni, avviano la riproduzione:

# Selezione playlist Spotify
- alias: Music_Spotify_Riproduci_playlist
  trigger:
    - platform: state
      entity_id: input_select.radio_spotcast_playlist
  condition:
    condition: and
    conditions:
      - condition: template
        value_template: >
                    {{ is_state("input_select.radio_spotcast_source", "Spotify") }}
      - condition: template
        value_template: >
                    {{ not is_state("input_select.radio_player", "Spento") }}  
      - condition: template
        value_template: >
                    {{ not is_state("input_select.radio_spotcast_playlist", "Spento") }}              
  action:
    - delay: "00:00:05" #attesa effettiva attivazione speaker
    - service: spotcast.start
      data_template:
        entity_id: >
                    {{ states['sensor.audio_speaker_selezionato'].state }}
        uri: >            
          {%-if is_state("input_select.radio_spotcast_playlist", "Daily Mix 1") %} spotify:playlist:playlistuniqueidentifier
          {%-elif is_state("input_select.radio_spotcast_playlist", "Daily Mix 2") %} spotify:playlist:playlistuniqueidentifier
          ...
          {% endif %}
        random_song: true
        shuffle: true
# Selezione stream web radio
- alias: Music_Radio_Riproduci_stazione
  trigger:
    - platform: state
      entity_id: input_select.radio_station
  condition: 
    condition: and
    conditions:
      - condition: template
        value_template: >
                    {{ is_state("input_select.radio_spotcast_source", "Radio") }}      
      - condition: template
        value_template: >
                    {{ not is_state("input_select.radio_player", "Spento") }}
      - condition: template
        value_template: >
                    {{ not is_state("input_select.radio_spotcast_playlist", "Spento") }}             
  action:
    - service: media_player.play_media
      data_template:
        entity_id: >
                    {{ states.sensor.audio_speaker_selezionato.state }} 
        media_content_id: >        
          {%-if is_state("input_select.radio_station", "Radio Deejay") %} http://radiodeejay-lh.akamaihd.net/i/RadioDeejay_Live_1@189857/master.m3u8
          {%-elif is_state("input_select.radio_station", "Rai Radio 1") %} http://icestreaming.rai.it/1.mp3
          {%-elif is_state("input_select.radio_station", "Rai Radio 2") %} http://icestreaming.rai.it/2.mp3
          ...
          {% else %}
            ''
          {% endif %}
        media_content_type: "music"         

Procediamo nel file auto_music_player.yaml ad implementiamo tutte le altre funzioni basen necessarie. Durante la riproduzione è possibile cambiare il player utilizzato senza perdere la selezione della sorgente e del contenuto che si sta ascoltando:

- alias: Music_Cambia_player
  trigger:
    - platform: state
      entity_id: input_select.radio_player
  condition: >
        {{ not is_state("input_select.radio_player", "Spento") }} 
  action:
    - service: media_player.turn_off
      data:
        entity_id:
          - media_player.tutta_casa
          - media_player.diffusione
          ...
    - delay: '00:00:01'          
    - service: media_player.turn_on
      data_template:
        entity_id: >
                    {{ states.sensor.audio_speaker_selezionato.state }} 
    - delay: '00:00:01'          
    - service: automation.trigger
      data_template:
        entity_id: >
            {%-if is_state("input_select.radio_spotcast_source", "Spotify") %}
              automation.music_spotify_riproduci_playlist 
            {% else %}
              automation.music_radio_riproduci_stazione
            {% endif %}               

A seguire è implementato lo spegnimento da interfaccia:

  - alias: Music_Spegni_Musica
    trigger:
      - platform: state
        entity_id: input_select.radio_spotcast_source
        to: "Spento"
    condition: >
            {{ not is_state("input_select.radio_player", "Spento") }}
    action:
      - service: media_player.turn_off
        data:
          entity_id:
            - media_player.tutta_casa
            ...
      - service: input_select.select_option
        data:
          entity_id: input_select.radio_player
          option: 'Spento'
      - service: input_select.select_option
        data:
          entity_id: input_select.radio_station
          option: 'Spento'
      - service: input_select.select_option
        data:
          entity_id: input_select.radio_spotcast_playlist
          option: 'Spento' 

Ed infine, una comoda funzione di spegnimento automatico nel caso di interruzione della riproduzione dall’esterno (ad esempio via comando vocale):

  - alias: Music_Riproduzione_finita
    trigger:
      - platform: state
        entity_id: binary_sensor.audio_riproduzione
        to: "off"
        for: "00:01:00" #considera spento trascorso questo tempo
    action:
      - service: input_select.select_option
        data:
          entity_id: input_select.radio_spotcast_source
          option: "Spento"  

Interfaccia: il layout della plancia

Passiamo adesso a disegnare l’interfaccia del nostro Music Player che sarà una pagina del nostro Home Assistant. Per impostare il layout della dashboard usiamo la custom:layout-card, con la quale organizziamo le card in tre aree, lasciando una riga in alto come spazio vuoto regolabile all’occorrenza:

  • header con tutti i pulsanti di controllo
  • panel1 per il controllo dei device disponibili
  • panel2 con le info sulla riproduzione in corso
  - type: custom:layout-card
    layout_type: custom:grid-layout
    layout:
      grid-template-columns: auto 450px 400px auto
      grid-template-rows: 5px 80px 400px
      grid-gap: 5px
      grid-template-areas: |
        ". . . ."
        "header header header header"
        ". panel1 panel2 ."        
    cards:
      # Pulsanti di controllo
      - type: horizontal-stack
        view_layout: 
          grid-area: header
        cards:
          ...
      # Controllo singoli players
      - type: custom:stack-in-card
        mode: vertical
        view_layout: 
          grid-area: panel1              
        cards:
          ...          
      # Pannello riproduzione
      - type: custom:stack-in-card
        view_layout: 
          grid-area: panel2   
        mode: vertical          
        cards: 
          ...

Interfaccia: i pulsanti di controllo

Sfuttiamo il meccanismo dei template a corredo della custom:button-card per modellare i pulsanti della nostra interfaccia, in modo da riutilizzare la maggior parte di codice possibile, andando a snellire la definizione della pagina stessa. Partiamo con i template dei pulsanti di selezione delle stazioni radio, con o senza logo:

  # Pulsante stazione radio solo nome
  radio_preset:  
    variables:
      var_name: "Radio Name"
      var_option: ""
    entity: input_select.radio_station
    name: '[[[ return variables.var_name ]]]'
    show_name: true
    show_icon: false
    tap_action:
      action: call-service
      service: input_select.select_option
      service_data:
        entity_id: input_select.radio_station
        option: '[[[ return variables.var_option=="" ? variables.var_name : variables.var_option ]]]'
    styles:
      card: [height: 40px]
      name:
        - font-size: 10pt
        - color: >
            [[[ 
              if (entity.state==variables.var_name || entity.state==variables.var_option)
                return 'lime'
              return ''
            ]]]            
  # Pulsante stazione radio con aggiunta del logo         
  radio_preset_logo:
    template: ['radio_preset']
    layout: icon_name
    entity_picture: > 
      [[[ 
        var logo=''
        var selected=(variables.var_option=='' ? variables.var_name : variables.var_option)           
        if (selected=="Radio Deejay")
          logo='RadioDeejay.png'
        else if (selected=="Rai Radio 1")
          logo='RaiRadio1.png'
        else if (selected=="Rai Radio 2")
          logo='RaiRadio2.png'
        ...
        else
          logo='RadioWWW.png'      
        return '/local/radio/60X37/' + logo 
      ]]]

Continuiamo con il template per il generico pulsante, che ne definisce l’estetica di base:

  radio_button:
    color_type: icon
    color: var(--paper-card-background-color)
    show_name: false    
    styles:
      card: [border-radius: 50%, height: 60px, width: 60px]

Il template del pulsante per la selezione di una voce nei vari selettori, ne specifica il comportamento, ovvero l’azione eseguita ed il conseguente aspetto assunto:

  radio_button_select:
    template: ['radio_button']
    variables:
      value_select: "Spento"
      color_selected: "lime"
      color_unselected: ""
    tap_action:
      action: call-service
      service: input_select.select_option
      service_data:
        entity_id: '[[[ return entity.entity_id ]]]'
        option: '[[[ return variables.value_select ]]]'  
    styles:
      icon:
        - size: 100%
        - color: >
            [[[       
              if (entity.state==variables.value_select)
                return variables.color_selected
              else
                return variables.color_unselected
            ]]]               

Infine il template per i pulsanti di seek dei contenuti:

  radio_seek_button:
    template: ['radio_button']
    variables:
      var_dir: "next"
    color: var(--paper-card-background-color)         
    icon: >
            [[[ return 'mdi:skip-' + variables.var_dir ]]]
    tap_action:
      action: call-service
      service: >
                [[[ return 'input_select.select_' + variables.var_dir ]]]
      service_data:
        entity_id: input_select.radio_station

Siamo pronti quindi ad istanziare la nostra pulsantiera, usando i template sopra definiti. Ad esempio i pulsanti di selezione della sorgente e dello speaker, da inserire nella pila orizzontale di area header :

  # Esempio selezione sorgente         
  - type: custom:button-card
    template: radio_button_select
    variables:
      value_select: "Radio"
    icon: mdi:radio
    entity: input_select.radio_spotcast_source
    
  # Esempio selezione player 
  - type: custom:button-card
    template: radio_player_select
    variables:
      value_select: "Tutta_casa"
    icon: mdi:home
    entity: input_select.radio_player

Questi esempi vanno applicati a tutte le possibili opzioni dei nostri selettori, quindi per le tre sorgenti e per ciascuno speaker.

Interfaccia: la gestione della riproduzione

Nell’area panel1 posizioniamo i controlli dei singoli speaker per agire, durante la riproduzione, singolarmente su ognuno di essi. Allo scopo, utilizziamo una card entities nella quale includiamo tante righe custom:slider-entity-raw quanti sono i dispotivi/gruppi da gestire. Dividiamo i gruppi dai dispositivi con un elemento di tipo divider e lo stesso facciamo tra gli speaker fisici ed il media_player.spotify che posizioniamo in fondo alla lista. Con quest’ultimo potremo controllare in ogni momento la riproduzione (se attiva) sul nostro account Spotify.

  # controllo volume singoli players
  - type: custom:stack-in-card
    mode: vertical
    view_layout: 
      grid-area: panel1              
    cards: 
      - type: entities
        entities:   
        - type: custom:slider-entity-row
          entity: media_player.tutta_casa
          icon: mdi:home
          name: Tutta casa
          hide_state: false 
          state_color: true
          ...
        - type: divider
        - type: 'custom:mini-media-player'
          entity: media_player.spotify
          name: Spotify  
          artwork: material

Nel pannello panel2 (a destra) andiamo a visualizzare le informazioni di riproduzione. Per far questo usiamo card di tipo conditional con i vari contenuti, la cui visibilità è legata allo stato dei sensori che abbiamo definito in precedenza.

Ad esempio, il pannello di riproduzione della Radio mostra il nome ed il logo della radio on-air con i pulsanti per passare alle stazioni precedente/successiva nella lista:

  # pannello RADIO playing
  - type: conditional
    conditions:
      - entity: input_select.radio_spotcast_source
        state: "Radio"                       
    card:
      type: custom:stack-in-card
      cards:
        - type: custom:button-card
          show_name: false
          show_label: false      
          show_icon: true
          icon: mdi:music
          show_entity_picture: >
            [[[ 
              if (states['binary_sensor.audio_riproduzione'].state=='off')
                return "false"
              else
                return "true"
            ]]]            
          size: 90
          entity_picture: >
                        [[[ return '/local/radio/' + states['sensor.radio_preset_logo'].state ]]]               
          styles:
            card: [height: 339px, background: transparent]
            entity_picture: [height: 98%, width: 98%]
          tap_action:
            action: fire-dom-event
            browser_mod:
              command: popup
              title: Elenco stazioni radio
              large: true
              hide_header: false
              deviceID:
                - this
              card:
                !include popup_radio_spotcast.yaml                            
        # Seek Radio
        - type: custom:stack-in-card
          mode: horizontal           
          cards:
            # Previous station
            - type: custom:button-card
              template: radio_seek_button
              variables:
                var_dir: "previous"
            # On-air station
            - type: custom:button-card
              color_type: card
              color: var(--paper-card-background-color)          
              show_icon: false
              name: "Riproduzione"
              show_label: true
              label: >
                                [[[ return states['input_select.radio_station'].state ]]]
              tap_action:
                action: fire-dom-event
                browser_mod:
                  command: popup
                  title: Elenco stazioni radio
                  large: true
                  hide_header: false
                  deviceID:
                    - this
                  card:
                    !include popup_radio_spotcast.yaml
              styles:
                label:
                  - color: lime
                card:
                  - height: 60px
            # Next station
            - type: custom:button-card
              template: radio_seek_button
              variables:
                var_dir: "next"

Interfaccia: popup con le playlist

Ultimo tassello: il popup con le stazioni radio o le playlist Spotify. Utilizziamo il componente browser_mod per attivare il popup, per il cui contenuto usiamo invece una card conditional che mostra le stazioni radio o le playlist in base alla selezione dell’utente:

  # POPUP Content
  type: custom:state-switch
  entity: input_select.radio_spotcast_source  
  states:
    Spotify:
      ...  
    Radio:
      ...

Nel caso in cui si stia ascoltando la Radio, il popup contiene tanti pulsanti con template radio_preset_logo definito in precedenza, quanti sono i canali radio disponibili, ad esempio:

  type: custom:layout-card
  layout_type: custom:horizontal-layout
  layout:
    width: 190
    max_cols: 5
  cards:
    - type: custom:button-card
      template: ['radio_preset_logo']
      variables:
        var_name: "Deejay Suona Italia"
        var_option: "Radio Deejay Suona Italia" 
    ...

Se invece è selezionato Spotify, il popup mostra l’elenco delle playlist disponibili:

  type: custom:select-list-card
  entity: input_select.radio_spotcast_playlist
  title: Spotify playlist
  icon: 'mdi:playlist-music'
  max_options: 10
  scroll_to_selected: true
  truncate: true 

Risultato finale

Il codice completo è disponibile qui.

Il risultato finale è mostrato in questo breve video:

Enjoy!