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.
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.
|
|
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).
|
|
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.
|
|
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"
trigger.event.data.device_id
returns the ID of the tag reader that triggered the eventpassing 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" }
media_players[<device_id>].player_id
returns the player ID (duh)device_entities(<player_id>)
looks up and returns list of entities associated with the media player with the given ID.device_entities(<player_id>)[0]
optimistically assumes that the media player will only have one entity (that we’d care about)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.
although it would bring certain sense of adventure to the home automation. ↩︎
“slightly convoluted” won the first place in the Understatement of the Year contest, three years in a row. ↩︎
…in the house that Jack built. ↩︎
thank you, Capt. Obvious. ↩︎
Joke almost as horrible as having to maintain two copies of a table. ↩︎