Ubuntu – udev rule to run Python script

pulseaudiopythonudev

I am trying to automatically run this script, whenever I connect to my bluetooth headset.

I have created the file /etc/udev/rules.d/80-bt-headset.rules with the line

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50"

but it doesn't do anything. The conditions are fine, a simple test comand is triggered when I enter that instead. The script itself also works fine when run manually.

What is going wrong here?

Update: There is an error when running the script with sudo -u USER (see below for details). Could this be the problem? And how does sudo-ing to the same user break things?

Update 2: After replacing all instances of pacmd with pactl in a2dp.py (and replacing list-sinks with list sinks to make it a valid pactl command), sudo -u USER works, however, the udev rule still doesn't. In /var/log/syslog I just see the line

systemd-udevd[32629]: Process '/home/USER/.local/bin/a2dp_2.py 00:22:37:3D:DA:50' failed with exit code 1.

Update 3 (Solution): The modified skript (pacmd -> pactl, see Update 2) with the environment variables DISPLAY=:0 and XAUTHORITY=/home/USER/.Xauthority did the trick. The udev rule:

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" ENV{DISPLAY}=":0" ENV{XAUTHORITY}="/home/USER/.Xauthority" RUN+="/home/USER/.local/bin/a2dp_2.py 00:22:37:3D:DA:50"

is working as intended.

(Now the only remaining problem is, that the script itself will trigger the rule, as it reconnects the headset, resulting in an infinite loop. However, that is a separate question, and it should not be too hard to find a workaround. In fact I was expecting that behaviour when I started this thread.)

What works:

  1. The conditions are fine: The line:

    ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/bin/mkdir /tmp/testme"
    

    will create a new directory when I connect to the headset.

  2. The script a2dp.py itself works fine when run from the terminal via

    /home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50
    
  3. Running a simply Python script via udev:

    ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/home/USER/.local/bin/atestscript.py"
    

    where atestscript.py contains:

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    import subprocess
    
    def main():
        subprocess.Popen(['mkdir', '/tmp/atestdir'])
    
    if __name__ == '__main__':
        main()
    

    will again create a folder when the device is connected.

What works after replacing pacmd with pactl:

  1. Running the script from the terminal with sudo -u USER or even sudo -u root now works as intended
    (For the originial script this resulted in:

    USER@MACHINE:~$ sudo -u USER /usr/local/bin/a2dp.py 00:22:37:3D:DA:50
    Connection MADE
    Device MAC: 00:22:37:3D:DA:50
    Command: pacmd list-sinks failed with status: 1
    stderr: No PulseAudio daemon running, or not running as session daemon.
    
    Exiting bluetoothctl
    

What does not work:

  1. Running the script as above or with any of the following lines as the RUN+= part:

    /usr/bin/sudo -u USER /usr/bin/python3 /home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50
    /usr/bin/sudo -u USER /home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50
    /usr/bin/python3.5 /usr/local/bin/a2dp.py 00:22:37:3D:DA:50
    ENV{DISPLAY}=":0" RUN+="/usr/local/bin/a2dp.py 00:22:37:3D:DA:50"
    

    Even the modified script will not work:

    ENV{DISPLAY}=":0" ENV{PULSE_RUNTIME_PATH}="/run/user/1000/pulse/" RUN+="sudo -u USER /home/USER/.local/bin/a2dp_2.py 00:22:37:3D:DA:50"
    

Further information: udevadm monitor output on connecting to headset:

KERNEL[104388.664737] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
UDEV  [104388.667185] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
KERNEL[104390.848157] add      /devices/virtual/input/input46 (input)
UDEV  [104390.849150] add      /devices/virtual/input/input46 (input)
KERNEL[104390.849471] add      /devices/virtual/input/input46/event17 (input)
UDEV  [104390.864692] add      /devices/virtual/input/input46/event17 (input)

Best Answer

My working solution

  1. Modify a2dp.py by replacing all instances of pacmd with pactl adjusting pacmd list-sinks to pactl list sinks (in my case saved as /usr/local/bin/a2dp_2.sh).

  2. Create a wrapper script /usr/local/bin/a2dp-wrapper.sh

    #!/bin/bash
    
    MAC=$1
    MACMOD=$(echo $MAC | sed 's/:/_/g')
    
    PID=$(pgrep pulseaudio)
    USER=$(grep -z USER= /proc/$PID/environ | sed 's/.*=//')
    
    export DISPLAY=:0
    export XAUTHORITY=/home/$USER/.Xauthority
    
    if pactl list sinks short | grep "bluez_sink\.$MACMOD.*SUSPENDED" 
        then
        sudo -u $USER /usr/local/bin/a2dp_2.py $MAC
    fi
    
  3. Add the following line to /etc/udev/rules.d/80-bt-headset.rules:

    ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/usr/local/bin/a2dp-wrapper.sh $attr{name}"
    

This wrapper script accomplishes the following:

  1. It finds out the $USER owning the running instance of pulseaudio, then sets the environment variables DISPLAY=:0 and XAUTHORITY=/home/$USER/.Xauthority necessary for pactl to work. This should make it work for all users on a machine. (I have not tested the effects of multiple users logged in at the same time.)

  2. It checks whether the corresponding sink is suspended and only then runs a2dp_2.py. This is necessary to prevent an infinite loop caused by a2dp_2.py reconnecting the device and thus triggering the rule.

  3. It runs a2dp_2.py as $USER. If run as root, a2dp_2.py will leave pulseaudio, and thus any audio settings, inaccessible without root privileges.

Alternatives: dbus loop/fixed package

  1. An alternative solution using a dbus loop can be found on the sript developer's page.

  2. A fix for the original bug is now available here and can be easily installed by adding ppa:ubuntu-audio-dev/pulse-testing and updating the available packages.

Hint: Finding your device's MAC address

Not strictly part of the original problem, but this might be useful for future reference. There are numerous ways to find your device's MAC address. The following is the one I consider most helpful for udev rules:

  1. Find the device path by running udevadm monitor and then connecting your device. Your output should look something like this:

    USER@MACHINE:~$ udevadm monitor
    monitor will print the received events for:
    UDEV - the event which udev sends out after rule processing
    KERNEL - the kernel uevent
    
    KERNEL[123043.617276] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
    UDEV  [123043.647291] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
    KERNEL[123044.153776] add      /devices/virtual/input/input68 (input)
    KERNEL[123044.153911] add      /devices/virtual/input/input68/event17 (input)
    UDEV  [123044.193415] add      /devices/virtual/input/input68 (input)
    UDEV  [123044.213213] add      /devices/virtual/input/input68/event17 (input)
    

    Stop the monitor with Ctrl+C. We have found three device paths. The one relevant for us is /devices/virtual/input/input68.

  2. Plug the obtained path into udevadm info:

    USER@MACHINE:~$ udevadm info -a -p /devices/virtual/input/input68
    
    Udevadm info starts with the device specified by the devpath and then
    walks up the chain of parent devices. It prints for every device
    found, all possible attributes in the udev rules key format.
    A rule to match, can be composed by the attributes of the device
    and the attributes from one single parent device.
    
      looking at device '/devices/virtual/input/input68':
        KERNEL=="input68"
        SUBSYSTEM=="input"
        DRIVER==""
        ATTR{name}=="00:22:37:3D:DA:50"
        ATTR{phys}==""
        ATTR{properties}=="0"
        ATTR{uniq}==""
    

    We learn that the MAC address is 00:22:37:3D:DA:50 and also that it is stored as ATTR{name}.

Even if the output looks completely different, these two commands will be a good start in looking for the relevant conditions for a udev rule.

Experimental: Catching arbitrary bluetooth audio devices

The rule:

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="??:??:??:??:??:??" RUN+="/usr/local/bin/a2dp-wrapper.sh $attr{name}"

will trigger for any input device that has a name attribute that looks like a MAC address and the conditional in the wrapper script should make sure no unintended actions are taken.

I do not have any other bluetooth audio device readily available to test this, but I see a number of potential issues:

  1. This will only work for a bluetooth device that is recognised as an input device containing the MAC address in the name attribute. Not every device may be recognised as such.

  2. This solution is not very elegant, as the rule will be triggered for any input device. However, I have not been able to find clear indicators to identify a bluetooth audio device. (As seen above, the input device has no further attributes, and the bluetooth device shows no indication of being an audio device, nor does it contain the MAC address. Maybe ACPI would be better for this.)

  3. You may not want to treat every bluetooth audio device the same: You may want to use the HSP protocol for your headset or you may not want to automatically switch to your housemate's speakers, that you've paired with at some point, whenever they are available. In those cases it is probably preferable to have a separate rule for each device.

I will keep updating this post as I learn more.