Side Project: Entrance Song

I’ve been working on a side project inspired by a professional wrestling and a blog post I read about a year ago.

I can’t remember the blog’s name, but it described how the local system administrator setup their Sonos speakers to play a theme song whenever an employee entered the office. They used DHCP to detect when an employee’s mobile phone connected to the corporate WiFi and then matched the MAC address to an employee and their chosen entrance song.

The whole idea was pretty amazing and it made me think of my favorite thing about professional wrestling: getting a chance to pick an entrance song for when you enter the ring. It’s something I give a lot of thought to.

This is essentially how I wanted to feel walking in my front door every day after work and also let my friends set their own entrance theme songs.

The implementation used in the blog used tcpdump and some bash scripts, but I wanted to write a version in Python.

Here’s the link to the GitHub repo.

Sniffing the DHCP Transactions

Detecting when someone enters my house is done using DHCP, which is a weird protocol. It’s based on UDP (and implicitly, IP) but it’s used for getting an IP address lease so the device likely doesn’t have an IP address. The solution for this is to just broadcast the DHCP request and response to everyone on the network. The end result is a very noisy transaction, but that makes it easy to sniff.

The whole thing is like walking into a hotel lobby and shouting at everyone present until the conceirge hears you and gives you a room key.

For our entrance song to detect a device, we just need to listen for that DHCPREQUEST packet, since it’s being broadcast everywhere.

I’m using Scapy, a very awesome Python library, to sniff my network for DHCP traffic. Scapy is pretty well known for doing cyber security stuff like crafting packets and building sniffers. It’s very easy to write a basic sniffer with a callback.

from scapy.all import Ether, DHCP, sniff

sniff(prn=self.dhcp_monitor_callback, filter='udp and (port 67 or 68)', store=0)

def dhcp_monitor_callback(pkt):
    """Callback for DHCP requests"""
    if not pkt.haslayer(DHCP):
        return
    if pkt[DHCP].options[0][1] != 3:
        return

    mac_addr = pkt[Ether].src
    logging.info('DHCP request from %s', mac_addr)

Here, we’re sniffing for UDP on port 67 and 68 and passing the packet to a callback function. In the callback, we’re making sure it’s actually a DHCP packet and that it’s a DHCPREQUEST packet (the options are a list of tuples of DHCP key-value options. The first tuple is the type of DHCP message, here we’re using type 3 for DHCP requests).

After verifying that we have a DHCP request, we can easily pull the MAC address out of the Ethernet layer. We’ll use this later to compare to my friend’s names in a SQLite database and figure out what song to play.

device = data.get_device_by_mac_addr(mac_addr)
if not device:
    logging.info('This is not a device I know about... Adding it to the database')
    data.insert_device(mac_addr)
    return

if device.owner.song:
    song = device.owner.song
    logging.info('%s is about to enter (%s)! playing %s by %s', \
        device.owner.name,
        device.friendly_name,
        song.title,
        song.artist)
else:
    logging.info('Device owner %s does not have a song. Doing nothing.', device.owner.name)
    return

Meanwhile the SQLAlchemy models are pretty standard classes that represent a Device, an Owner, and a Song and the foreign keys between them. The data.get_device_by_mac_addr and data.insert_device are just shortcut functions for querying and inserting data into my database.

Playing Music

Once we’ve determined who’s entered my front door, we need to play their theme song. I have a Spotify premium account, so I can literally play anything. Spotify provides a pretty good web-based REST API and there’s a Python wrapper for it called Spotipy.

Spotipy hasn’t been updated in a while and the original author doesn’t seem to be merging pull requests. There are a few issues with the library so I forked it and patched them up myself.

Authenticating my premium Spotify account using Spotipy is a little weird. You have to register for their developer program and create a new project. After that, you need to allow the project to access your Spotify account with the appropriate level of access.

For playing the music, we want to do the following:

  1. Look up who entered my front door and see if they have an entrance song.
  2. If so, save the current Spotify playback so we can resume it later.
  3. Fade out the current song.
  4. Set the volume high and blast that entrance song for the next 45 seconds or so
  5. Fade out the entrance song
  6. Fade in the previously played music prior to the entrance and return everything back to normal

Sounds easy enough. There are a few interesting problems to solve though. Spotify doesn’t have a way to play just a section of music and then stop. We can fake this out by starting the music in a new thread, sleeping in that thread for the entrance duration (usually about 45 seconds to a minute) and then stop the music right before ending the thread.

class MusicThread(Thread):
    """A thread to start music and sleep. This is a cheap way to implement playing
    a duration of a song since the Spotify API doesn't include that.
    """
    def __init__(self, sp_context, mp_context, uri, position_ms, duration=45,
                 device_id=None):
        """Constructor
        Args
            sp_context - The spotify context
            mp_context - music player context
            uri - The URI to play
            position_ms - The position in ms to start from
            duration - The duration to play, in seconds
            device_id - The device to play on, or None to play on the default device
        """
        super().__init__()

        self.sp = sp_context
        self.mp = mp_context
        self.uri = uri
        self.position_ms = position_ms
        self.duration = duration
        self.device_id = device_id

    def run(self):
        """Runs the thread.
        Starts playback on the track, sleeps, and then fades out the track.
        """
        logging.info('Starting playback in new thread')
        try:
            self.sp.pause_playback()
        except SpotifyException as e:
            # This often happens if there is no current active device. We'll assume there's
            # device_id being used. The next try/catch block will handle it if not.
            pass
        try:
            self.sp.start_playback(device_id=self.device_id, uris=[self.uri],
                                   position_ms=self.position_ms)
        except SpotifyException as e:
            logging.error(e)
            return
        self.mp.set_volume(self.mp.default_volume)

        logging.info('Putting the thread to sleep for %s seconds', self.duration)
        sleep(self.duration)
        logging.info('Stopping playback')

        # Get the currently playing tack to be sure we're stopping this track and not
        # someone else's.
        current_track = self.sp.currently_playing()
        try:
            uri = current_track['item']['uri']
            if uri == self.uri:
                self.mp.fade_out()
                self.sp.pause_playback()
            else:
                logging.info('Attempted to stop song %s but it\'s not playing', self.uri)
        finally:
            return

Another problem is figuring out how to handle if someone shows up while another person’s entrance song is already playing. Should the previous person’s music be cut off? That’s disrespectful. Also the threads might start fighting over the music controls and we’d get into a weird state.

I thought about writing some state machine code to keep the threads in sync but there’s a much more elegant solution: using Python’s Queue class. These things deserve way more credit than they get. They allow you to have a thread safe way to manipulate a collection. In my case I want to add things without blocking (i.e. someone walks in the door so I need to queue up their song) while removing things with blocking (i.e. making Spotify play the song but block until the song is complete before starting the next one). No awful state machine required!

Here’s the main loop for the music player:

def player_main(self):
    logging.info('Starting music player')
    while True:
        uri, start_minute, start_second, duration = self.song_queue.get(True)
        logging.info('Found a song on the queue!')
        logging.info('Playing %s at %d:%d duration %d', uri, start_minute, start_second,
                     duration)
        self.save_current_playback()
        t = self._play_song(uri, start_minute, start_second, duration)
        logging.info('Waiting for song to end...')
        t.join()
        self.restore_playback()
        logging.info('Song over... waiting for the next song on the queue')

The get method will block the thread until it sees something on the queue. The join method on the MusicThread will also block the main thread until the song completes. The MusicPlayer itself is a Thread so the whole thing doesn’t block the rest of my application while calling these methods.

My Scapy DHCP sniffing code just needs to drop songs on the queue and they will get picked up by the music player thread.

Results and Conclusion

It’s honestly pretty awesome to walk into your own house and have an entrance song play. I now get pumped up as I enter my door and start waving my hands to an imaginary crowd. I’m not sure what my neighbors think of this.

Other Things

Future Work

Appendix: What my friends picked as their entrance theme songs