Skip to main content
  1. Posts/

RFID Jukebox: Home Assistant Automation

·1775 words·9 mins·
Projects Lights Smart Home DIY Music Home Assistant Music Assistant
Table of Contents
RFID Jukebox - This article is part of a series.
Part 4: This Article
Finally, it’s time to actually create the jukebox. Everything else so far was just a fancy RFID tag reader with extra steps.

One the Home Assistant side of things, everything boils down to two automations, one of which is nearly identical to the one shown in the Home Assistant blog article on tags.

Just as a reminder, here’s an overview of how the Jukebox interacts with the Home Assistants.

Sequence diagram depicting interactions between the Jukebox device, Home Assistant, and the automations
High level view always helps.
This assumes that you have (or know how to) installed and set up both Home and Music Assistant, and configure the ESPHome integration. If that’s not the case, refer to Home/Music Assistant help pages.

Home Assistant Automation
#

Starting the Jukebox
#

Automations in Home Assistant consist of three main parts: trigger(s), condition(s), and action(s).

As for the trigger, this automation has an extremely simple one - every time a tag is scanned, automation is triggered.

1
2
3
4
alias: Jukebox Start
triggers:
  - trigger: event
    event_type: tag_scanned

But we don’t want to let any tag reader to trigger this automation1 - just the allowed ones.

An allow list can be created using variables. One list - media_players - to associate the tag scanner with the player. The second list - to define tag IDs which trigger music playback (and which media it should play).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
variables:
  media_players:
    # ID of a the tag reader in Home Assistant
    58be1b5e4695e7270486bc7af97c3554:
      player_entity: media_player.squeezelite_239ad4
      # ID of the media player in Home Assistant
      player_id: 1e0dbd5f8b410f3ab9cd24320be02dde
  tags:
    45-AE-85-2A:   # ID of the RFID tag
      media_content_id: <id of a content>
      media_content_type: playlist
mode: single
max_exceeded: silent

You can check the ID of the devices by going to device’s configuration screen in the Home Assistant (for example by pressing d and typing the configured name of the device, or by clicking through the configuration menu, or however you like). The device ID will be in the browser’s address bar:

https://<address>/config/devices/device/<device id>

The tag ID can be read either from logs printed by the jukebox, or using an NFC reader phone app (like the NFC Tools), a Flipper Zero, or any other tag scanner.

That takes care of the triggering part of an automation. Now, conditions: exercising defensive programming approach, we can check if the device which generated the tag_scanned event is on the allow list defined in the variables.

1
2
3
4
5
conditions:
  - condition: and
    conditions:
      - condition: template
        value_template: "{{ trigger.event.data.device_id in media_players }}"

Additionally, it’s a good idea to check if the media player is currently playing. We don’t want the playback to randomly restart if a spurious trigger happens.

- condition: template
  value_template: >-
    {{ states(device_entities(media_players[trigger.event.data.device_id].player_id)[0]) != "playing"
    }}

The expression in the template is slightly convoluted2, I’ll admit that. Let’s break it down.

states(
  device_entities(
    media_players[
      trigger.event.data.device_id
    ].player_id
  )[0]
) != "playing"
  1. trigger.event.data.device_id returns the ID of the tag reader that triggered the event

  2. passing the tag reader’s ID to the media_players mapping returns the media player mapped to the tag reader:

     {
       "player_entity": "media_player.squeezelite_239ad4",
       "player_id": "1e0dbd5f8b410f3ab9cd24320be02dde"
     }
    
  3. media_players[<device_id>].player_id returns the player ID (duh)

  4. device_entities(<player_id>) looks up and returns list of entities associated with the media player with the given ID.

  5. device_entities(<player_id>)[0] optimistically assumes that the media player will only have one entity (that we’d care about)

  6. Finally, states(device_entities(<player_id>)[0]) returns the current state of the media player entity that’s mapped to the tag reader that triggered the event3.

Now that the conditions are checked and everything checks out, actions can be performed. To make it easier on our sanity, we can define some local (as in, to be used in the actions) variables.

actions:
  - variables:
      media_player_entity_id: "{{ media_players[trigger.event.data.device_id].player_entity }}"
      media_player_device_id: "{{ media_players[trigger.event.data.device_id].player_id }}"
      media_content_id: "{{ tags[trigger.event.data.tag_id].media_content_id }}"
      media_content_type: "{{ tags[trigger.event.data.tag_id].media_content_type }}"

And finally, the actions.

  - action: media_player.clear_playlist
    metadata: {}
    data: {}
    target:
      device_id: "{{ media_player_device_id }}"
  - action: media_player.shuffle_set
    metadata: {}
    data:
      shuffle: true
    target:
      device_id: "{{ media_player_device_id }}"
  - action: media_player.play_media
    target:
      entity_id: "{{ media_player_entity_id }}"
    data:
      media_content_id: "{{ media_content_id }}"
      media_content_type: "{{ media_content_type }}"

Here’s the complete YAML content of the automation:

alias: Start Jukebox
max_exceeded: silent
mode: single
description: |-
  Maps tags to media content and tag readers to media players.
  When a recognized tag is scanned, starts playing media on the selected player.
triggers:
  - trigger: event
    event_type: tag_scanned
conditions:
  - condition: and
    conditions:
      - condition: template
        value_template: "{{ trigger.event.data.device_id in media_players }}"
      - condition: template
        value_template: >-
          {{
          states(device_entities(media_players[trigger.event.data.device_id].player_id)[0])
          != "playing"

          }}
actions:
  - variables:
      media_player_entity_id: "{{ media_players[trigger.event.data.device_id].player_entity }}"
      media_player_device_id: "{{ media_players[trigger.event.data.device_id].player_id }}"
      media_content_id: "{{ tags[trigger.event.data.tag_id].media_content_id }}"
      media_content_type: "{{ tags[trigger.event.data.tag_id].media_content_type }}"
  - action: media_player.clear_playlist
    metadata: {}
    data: {}
    target:
      device_id: "{{ media_player_device_id }}"
  - action: media_player.shuffle_set
    metadata: {}
    data:
      shuffle: true
    target:
      device_id: "{{ media_player_device_id }}"
  - action: media_player.play_media
    target:
      entity_id: "{{ media_player_entity_id }}"
    data:
      media_content_id: "{{ media_content_id }}"
      media_content_type: "{{ media_content_type }}"
variables:
  media_players:
    <tag_reader_id>:
      player_entity: media_player.<entity_id>
      player_id: <media_player_id>
  tags:
    <tag_id>:
      media_content_id: <media content id>
      media_content_type: playlist

Media content ID must uniquely identify the media4. I’ve been using names of the Spotify playlists, works just fine.

Stopping the Playback
#

To make the playback stop when the card is removed we need to monitor the status of the Tag Active sensor.

alias: Stop the Jukebox
description: ""
triggers:
  - type: turned_off
    device_id: <id of the tag reader>
    entity_id: <id of the tag_active sensor>
    domain: binary_sensor
    trigger: device
conditions:
  - condition: device
    device_id: 1e0dbd5f8b410f3ab9cd24320be02dde
    domain: media_player
    entity_id: 492d8cae5747a15b70024ba96e93caee
    type: is_playing
actions:
  - action: media_player.media_pause
    metadata: {}
    data: {}
    target:
      device_id: 1e0dbd5f8b410f3ab9cd24320be02dde
mode: single

But wait! How to know which device is associated with the tag reader that triggered the event? What’s more, how to know which sensors to monitor here? After all, the other automation, for starting the playback, monitors all tag_scanned events, and filters them out based on the look-up tables.

There are two solutions. The first one, is to do something dreadful: manually hardcode which tag readers to monitor (for example, the ones that have the “Tag Active” sensor), and - even worse - create a copy of the look-up table in the “Stop the Jukebox” automation.

Just horrible; now to add a new card you’d have to remember to update at least two separate automation configs: one for starting the music, the other for stopping. And if you forget about the latter? You’d have a “Don’t Stop the Music” situation5.

The other solution is to extract the mappings of the tag readers to players and tag IDs to media content to a common place. Luckily, the Home Assistant lets you define scripts.

Scripts are simply sequences of actions that can be shared across automations. Perfect! Let’s define a script that creates the mappings and returns them to a response variable. This way, the automation that calls the script will have access to data returned by the script.

alias: Jukebox Mappings
description: "Returns mapping of tag reader to media player and tags to media."
mode: single
variables:
  mappings:
    media_players:
      58be1b5e4695e7270486bc7af97c3554:
        player_entity: media_player.squeezelite_239ad4
        player_id: 1e0dbd5f8b410f3ab9cd24320be02dde
    tags:
      45-AE-85-2A:
        media_content_id: <id of a content>
        media_content_type: playlist
sequence:
  - stop: Done
    response_variable: mappings

Here we go. Script only defines the variable called mappings, which has two keys: media_players and tags. media_players map tag reader’s device ID to a media player entity, while tags has a map of tag ID to media content.

Now in the actual automation we could call the script to get to the data:

alias: Stop the Jukebox
description: ""
triggers:
  - type: turned_off
    device_id: 58be1b5e4695e7270486bc7af97c3554
    entity_id: binary_sensor.frozen_nightlight_tag_active
    domain: binary_sensor
    trigger: device
actions:
  - action: script.jukebox_tags
    metadata: {}
    data: {}
    response_variable: mappings
  - if:
      - condition: template
        value_template: >-
          {{ 
           states(
            device_entities(
              mappings.media_players[trigger.event.data.device_id].player_id)[0]
            ) == "playing"
          }}
    then:
      - action: media_player.media_pause
        metadata: {}
        data: {}
        target:
          device_id: "{{ mappings.media_players[trigger.event.data.device_id].player_id }}"
mode: single

But the problem #1 isn’t solved yet, this automation is still hardcoded to a single tag reader. Not optimal.

Incremental Improvements
#

Shared Device Mappings
#

Let’s circle back and fix the “start” automation to use the mappings script, first.

alias: Start Jukebox
max_exceeded: silent
mode: single
description: |-
  Maps tags to media content and tag readers to media players.
  When a recognized tag is scanned, starts playing media on the selected player.
triggers:
  - trigger: event
    event_type: tag_scanned
conditions: null
actions:
  - action: script.jukebox_tags
    metadata: {}
    data: {}
    response_variable: mappings
  - variables:
      media_player_entity_id: "{{ mappings.media_players[trigger.event.data.device_id].player_entity }}"
      media_player_device_id: "{{ mappings.media_players[trigger.event.data.device_id].player_id }}"
      media_content_id: "{{ mappings.tags[trigger.event.data.tag_id].media_content_id }}"
      media_content_type: "{{ mappings.tags[trigger.event.data.tag_id].media_content_type }}"
  - if:
    - condition: and
      conditions:
        - condition: template
          value_template: "{{ trigger.event.data.device_id in mappings.media_players }}"
        - condition: template
          value_template: >-
            {{
            states(device_entities(mappings.media_players[trigger.event.data.device_id].player_id)[0])
            != "playing"
            }}
    then:
      - action: media_player.clear_playlist
        metadata: {}
        data: {}
        target:
          device_id: "{{ media_player_device_id }}"
      - action: media_player.shuffle_set
        metadata: {}
        data:
          shuffle: true
        target:
          device_id: "{{ media_player_device_id }}"
      - action: media_player.play_media
        target:
          entity_id: "{{ media_player_entity_id }}"
        data:
          media_content_id: "{{ media_content_id }}"
          media_content_type: "{{ media_content_type }}"

Conditions check moved from the separate step to actions, one action to retrieve the mappings added, long templates became even longer.

Monitoring the Removed Tags
#

Now, back to the issue with stopping the playback. How to fix it to make it more generic and not tied to a single device? Ideally, it would have to be triggered by an event, same as the starting automation, and the triggering event would have to be not tied to any specific domain or entity.

ESPHome, being tightly integrated with the Home Assistant, has a component to trigger events in the Home Assistant it’s connected to. Which sounds just like what we need; the tag reader device could just send a custom event, say tag_removed when the card is no longer detected:

pn532:
  # ...
  on_tag_removed:
  - homeassistant.event:
      event: esphome.tag_removed
      data:
        tag: !lambda 'return x;'

The stopping automation would then monitor all tag_removed events and filter them out based on the device ID.

alias: Stop the Jukebox
description: ""
triggers:
  - trigger: event
    event_type: esphome.tag_removed
actions:
  - action: script.jukebox_tags
    metadata: {}
    data: {}
    response_variable: mappings
  - if:
      - condition: template
        value_template: >-
          {{ 
           states(
            device_entities(
              mappings.media_players[trigger.event.data.device_id].player_id)[0]
            ) == "playing"
          }}
    then:
      - action: media_player.media_pause
        metadata: {}
        data: {}
        target:
          device_id: "{{ mappings.media_players[trigger.event.data.device_id].player_id }}"
mode: single

Funny how sometimes you have to iterate the design because something was missed in the original concept.

Anyway, this completes the RFID Jukebox project, and the series of posts about it. If you’re still here, thanks for going through this, hope you’ll find this useful.


  1. although it would bring certain sense of adventure to the home automation. ↩︎

  2. “slightly convoluted” won the first place in the Understatement of the Year contest, three years in a row. ↩︎

  3. …in the house that Jack built. ↩︎

  4. thank you, Capt. Obvious. ↩︎

  5. Joke almost as horrible as having to maintain two copies of a table. ↩︎

RFID Jukebox - This article is part of a series.
Part 4: This Article

Related

RFID Jukebox: The Firmware
·2148 words·11 mins
Projects Lights ESPHome Smart Home DIY Music Home Assistant Music Assistant
No coding required! Well, almost.
RFID Jukebox: The Build
·1533 words·8 mins
Projects Lights Smart Home DIY Music Home Assistant Music Assistant
You can use so many cards with this jukebox! No need for a credit card, though.
RFID Jukebox: The Story
·2111 words·10 mins
Projects Lights Smart Home DIY Music Home Assistant Music Assistant
Is it a lamp? Is it a music player? No, it’s sup… err, something else.