├── .gitignore ├── examples └── action.d │ ├── disconnect.d │ └── 01-debug.script │ ├── connect.d │ └── 01-debug.script │ ├── 03-midi.rules │ ├── 01-scriptexec.py │ ├── 03-dp_hdmi_switch.rules │ └── 02-example.rules ├── mplugd ├── __init__.py ├── rulesparser.py ├── plugins │ ├── pi_portmidi.py │ ├── pi_udev.py │ ├── pi_xevents.py │ ├── edid.py │ └── pi_pulseaudio.py ├── header.py └── mplugd.py ├── mplugd.conf.example ├── setup.py ├── COPYRIGHT └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /examples/action.d/disconnect.d/01-debug.script: -------------------------------------------------------------------------------- 1 | ../connect.d/01-debug.script -------------------------------------------------------------------------------- /mplugd/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ "header", "rulesparser", "mplugd" ] 2 | 3 | #import os 4 | #import glob 5 | #__all__ = [ os.path.basename(f)[:-3] for f in glob.glob(os.path.dirname(__file__)+"/*.py")] -------------------------------------------------------------------------------- /mplugd.conf.example: -------------------------------------------------------------------------------- 1 | ; example configuration for mplugd 2 | ; 3 | ; either place this file in the mplugd.py folder, into 4 | : ~/.mplugd/ or into /etc/mplugd/ 5 | 6 | ;;; include the action.d example directory of a local installation 7 | ; action_directory+=../examples/action.d/ 8 | -------------------------------------------------------------------------------- /examples/action.d/connect.d/01-debug.script: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | c=1 4 | for i in "$@"; do 5 | echo "Arg $c: $i" 6 | c="$(( $c + 1 ))" 7 | done 8 | 9 | exit 0 10 | 11 | # if [[ "$0" == *"disconnect"* ]]; then 12 | # echo disconnect 13 | # xrandr --output $1 --off 14 | # pacmd set-default-sink 1 > /dev/null 15 | # else 16 | # echo connect 17 | # xrandr --output $1 --auto 18 | # xrandr --output $1 --same-as LVDS-0 19 | # pacmd set-default-sink 2 > /dev/null 20 | # fi 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import glob, os 3 | 4 | def get_datafiles(): 5 | l = [("share/mplugd", ["mplugd.conf.example"])] 6 | prefix = "share/mplugd/" 7 | for root, dirs, files in os.walk("examples/action.d/"): 8 | f = [] 9 | for filename in files: 10 | fullpath = os.path.join(root, filename) 11 | f.append(fullpath) 12 | l.append((prefix+root, f)) 13 | return l 14 | 15 | setup( 16 | name="mplugd", 17 | version="0.1", 18 | packages=['mplugd', 'mplugd.plugins'], 19 | data_files=get_datafiles(), 20 | entry_points = { 21 | "console_scripts": [ "mplugd=mplugd.mplugd:main", ], 22 | } 23 | ) -------------------------------------------------------------------------------- /examples/action.d/03-midi.rules: -------------------------------------------------------------------------------- 1 | ; enable the play LED on a BCD2000 device as long as the corresponding button 2 | ; is pressed 3 | 4 | [rule bcd2000_play_led_on] 5 | on_type=MidiNoteOn 6 | on_values=1b 7f 0 7 | true_portmidi_send_bcd2000=b0 9 7f 8 | 9 | [rule bcd2000_play_led_off] 10 | on_type=MidiNoteOn 11 | on_values=1b 0 0 12 | true_portmidi_send_bcd2000=b0 9 0 13 | 14 | 15 | ; enable the cue LED when a audio sink is muted else disable it 16 | ; if cue button is pressed, send the XF86AudioMute key code to Xorg 17 | 18 | [rule bcd2000_cue_led_on] 19 | on_type=MuteUpdated 20 | on_Mute=True 21 | true_portmidi_send_bcd2000=b0 a 7f 22 | 23 | [rule bcd2000_cue_led_off] 24 | on_type=MuteUpdated 25 | on_Mute=False 26 | true_portmidi_send_bcd2000=b0 a 0 27 | 28 | [rule bcd2000_cue_mute_audio] 29 | on_type=MidiNoteOn 30 | on_values=1a 7f 0 31 | true_exec=xdotool key XF86AudioMute -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | mplugd is a daemon that listens on events (e.g. xrandr or 2 | pulseaudio) and executes user-defined actions on certain events. 3 | Copyright (C) 2013 Mario Kicherer (http://kicherer.org) 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | http://www.gnu.org/licenses/gpl-2.0.txt 20 | -------------------------------------------------------------------------------- /examples/action.d/01-scriptexec.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # This action calls (shell) scripts after (dis-)connect event from the 8 | # corresponding folder 9 | # 10 | 11 | import Xlib.ext.randr as randr 12 | import os, subprocess 13 | 14 | connectdir="connect.d" 15 | disconnectdir="disconnect.d" 16 | 17 | def execute_script(mplugd, command): 18 | if not os.access(command[0], os.X_OK): 19 | if mplugd.verbose: 20 | print "Ignoring", command[0] 21 | return 22 | 23 | if mplugd.verbose: 24 | print "Executing script", command[0] 25 | 26 | subprocess.call(command) 27 | 28 | def process(mplugd, event): 29 | if mplugd.verbose: 30 | print "Executing python script:", __name__ 31 | 32 | connected = None 33 | 34 | # list of parameters that are passed to scripts 35 | command = ["filename", event.etype] 36 | if event.item: 37 | command.append(event.item.name) 38 | 39 | # 40 | # overwrite and add parameters with regard to event type 41 | # 42 | 43 | if event.etype == "OutputChangeNotify": 44 | command[2] = event.item.name 45 | if event.item.connection == randr.Disconnected and event.item.crtc != 0: 46 | connected = False 47 | command[1] = "OutputDisconnect" 48 | elif event.item.connection == randr.Connected and event.item.crtc == 0: 49 | connected = True 50 | command[1] = "OutputConnect" 51 | else: 52 | print "startscript error", event.item.connection, event.item.crtc 53 | return 54 | 55 | elif event.etype == "NewPlaybackStream": 56 | connected = True 57 | if event.item: 58 | command.append(getattr(event.item, "application.process.binary")) 59 | elif event.etype == "PlaybackStreamRemoved": 60 | connected = False 61 | if event.item: 62 | command.append(getattr(event.item, "application.process.binary")) 63 | else: 64 | if mplugd.verbose: 65 | print __name__, "unkown event, stopping" 66 | return 67 | 68 | # choose script directory 69 | if connected: 70 | directory = connectdir; 71 | else: 72 | directory = disconnectdir; 73 | 74 | # walk through directories and execute scripts 75 | for d in mplugd.config["action_directory"]: 76 | for root, dirs, files in os.walk(d+directory): 77 | files.sort() 78 | for filename in files: 79 | fullpath = os.path.join(root, filename) 80 | 81 | if fullpath.split(".")[-1] == "script": 82 | command[0] = fullpath 83 | execute_script(mplugd, command) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | mplugd 3 | ====== 4 | 5 | mplugd is a daemon that listens on events (e.g. xrandr, pulseaudio, ...) and 6 | executes user-defined actions on certain events. In contrast to other 7 | approaches, it listens to events by registering callback handlers *instead* of 8 | polling and parsing tool output, if possible. Event processing is done through 9 | a threaded producer/consumer architecture that can be extended by plugins to 10 | insert new event types. Actions can be defined using INI-like rule files or 11 | simple scripts. 12 | 13 | A common use-case is automatic configuration of plugged-in devices like HDMI 14 | or DisplayPort displays including switch of audio output using pulseaudio. It 15 | also enables automatic actions on udev events without root privileges. 16 | 17 | Requirements: 18 | 19 | * For pulseaudio: dbus-python 20 | * For X events: python-xlib (SVN revision > r160 or version > 0.15) 21 | * For udev: pyudev 22 | * For MIDI: pyportmidi 23 | 24 | Examples of supported events 25 | ---------------------------- 26 | 27 | * New/removed Xorg displays 28 | * Applications starting/ending audio output through PulseAudio 29 | * PulseAudio port changes (e.g., plugged-in headphones) 30 | * Udev events (e.g., new/removed devices or changed properties) 31 | * MIDI events 32 | 33 | Usage 34 | ----- 35 | 36 | 1. Place your rules/scripts either into `/etc/mplugd/action.d/`, 37 | `~/mplugd/action.d/` or `$SRCDIR/mplugd/action.d/`. For examples, look into 38 | `mplugd/examples/action.d/`. Only files ending with `.py` or `.rules` are 39 | considered. You can look-up the values for your current system using 40 | `mplugd -d`. 41 | 2. Start `mplugd`. In case you manually installed python-xlib, you might need 42 | to add its location to PYTHONPATH before starting mplugd. 43 | 44 | Rules example 45 | ------------- 46 | 47 | * If DP-[0-9] gets connected to a display with ID "SAMC900", change default 48 | sink to the sink known as "HDA NVidia" card to ALSA and set the display 49 | configuration to automatic. 50 | 51 | [rule on_dpX_connect] 52 | on_type=OutputChangeNotify 53 | on_name*=DP-[0-9] 54 | on_connected=1 55 | on_crtc=0 56 | on_id_string=SAMC900 57 | true_stream_set_defaultsink_to_alsa.card_name=HDA NVidia 58 | true_exec=xrandr --output %event_name% --auto 59 | 60 | * If a process with the binary `mplayer` starts audio output and if output DP-0 61 | is connected, move this stream to sink "HDA NVidia", display a message in 62 | the console and send a notification to the desktop. 63 | 64 | [rule move_new_mplayer_to_intel] 65 | on_type=NewPlaybackStream 66 | on_application.process.binary=mplayer 67 | if_output_DP-0_connected=1 68 | true_stream_move_event_to_alsa.card_name=HDA NVidia 69 | true_exec=echo "moving mplayer to HDA NVidia" 70 | true_exec=notify-send "mplugd moves mplayer to HDA NVidia" 71 | 72 | * If a device with a name matching `dvb[0-9].frontend[0-9]` appears and if 73 | block device `sdc` is present, start recording with mplayer 74 | 75 | [rule udev_example] 76 | on_type=UdevAdd 77 | on_name*=dvb[0-9].frontend[0-9] 78 | if_udev_device_block_name=sdc 79 | true_exec=echo "%event_type% device: %event_name%, starting record" 80 | true_exec_thread=mplayer dvb:// -dumpstream -dumpfile /mnt/sdc/dump.ts -------------------------------------------------------------------------------- /examples/action.d/03-dp_hdmi_switch.rules: -------------------------------------------------------------------------------- 1 | ; This is an example that automatically enables DisplayPort displays and 2 | ; switches audio output for PulseAudio-enabled applications 3 | 4 | ; Example: if DP-[0-9] gets connected to a display, change default sink and 5 | ; all streams from class "videoplayers" to "HDA NVidia" and set the 6 | ; display configuration to automatic 7 | [rule on_dp0_connect] 8 | on_type=OutputChangeNotify 9 | on_name*=DP-[0-9] 10 | on_connected=1 11 | ; if crtc != 0, the output was already configured. E.g., after executing 12 | ; the xrandr command, a second event occurs to indicate the output is now 13 | ; part of the crtc 14 | on_crtc=0 15 | 16 | true_stream_set_defaultsink_to_alsa.card_name=HDA NVidia 17 | true_stream_move_class_videoplayers_to_alsa.card_name=HDA NVidia 18 | true_exec=xrandr --output %event_name% --auto 19 | 20 | 21 | ; Example: if DP-[0-9] gets disconnected from a display, change default sink and 22 | ; all streams from class "videoplayers" to "HDA Intel PCH" and set the 23 | ; display configuration to off 24 | ; 25 | ; != negates a comparison 26 | [rule on_dp0_disconnect] 27 | on_type=OutputChangeNotify 28 | on_name*=DP-[0-9] 29 | on_connected=0 30 | ; if crtc == 0, the output was already deactivated 31 | on_crtc!=0 32 | 33 | true_stream_set_defaultsink_to_alsa.card_name=HDA Intel PCH 34 | true_stream_move_class_videoplayers_to_alsa.card_name=HDA Intel PCH 35 | true_exec=xrandr --output %event_name% --off 36 | 37 | 38 | ; Please note: there are other tools like mixers or PA itself that restore 39 | ; the output sink for a process. If such tools are active, setting the 40 | ; default sink has no effect. Instead, we actively set the output sink 41 | ; for new processes with the following two rules. 42 | 43 | ; Example: if a process matching stream_class "videoplayers" starts audio 44 | ; output, move this stream to sink "HDA NVidia" but only if output 45 | ; DP-0 is connected 46 | [rule move_new_mplayer_to_nvidia] 47 | on_type=NewPlaybackStream 48 | on_stream_class=videoplayers 49 | if_output_DP-0_connected=1 50 | true_stream_move_event_to_alsa.card_name=HDA NVidia 51 | true_exec=echo "moving %event_application.process.binary% to HDA NVidia" 52 | true_exec=notify-send "mplugd moves %event_application.process.binary% (PID %event_application.process.id%) to HDA NVidia" 53 | 54 | 55 | 56 | ; Example: if a process matching stream_class "videoplayers" starts audio 57 | ; output, move this stream to sink "HDA NVidia" but only if output 58 | ; DP-0 is disconnected 59 | [rule move_new_mplayer_to_intel] 60 | on_type=NewPlaybackStream 61 | on_stream_class=videoplayers 62 | if_output_DP-0_connected=0 63 | true_stream_move_event_to_alsa.card_name=HDA Intel PCH 64 | true_exec=echo "moving %event_application.process.binary% to HDA Intel PCH" 65 | true_exec=notify-send "mplugd moves %event_application.process.binary% (PID %event_application.process.id%) to HDA Intel PCH" 66 | 67 | 68 | ; Example: stream class "videoplayers" stands for all streams that belong to a 69 | ; process with a binary named "mplayer", "vlc" or "*player" 70 | ; 71 | ; *= allows regexp matching 72 | [stream_class videoplayers] 73 | stream_application.process.binary=mplayer 74 | stream_application.process.binary=vlc 75 | stream_application.process.binary*=player 76 | 77 | 78 | -------------------------------------------------------------------------------- /mplugd/rulesparser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # This is a custom INI-like configuration parser similar to ConfigParser 8 | # but besides "=" it also accepts "!=" and "*=" to match regular expressions, 9 | # as well as multiple values per key. 10 | # 11 | 12 | from __future__ import absolute_import 13 | from __future__ import print_function 14 | import re 15 | 16 | # extend str to store the separator 17 | class MyRulesValue(str): 18 | def __new__(cls,sep,value): 19 | obj = str.__new__(cls, value) 20 | obj.sep = sep 21 | return obj 22 | 23 | class MyRulesParser(object): 24 | seps = ["!=", "*=", "="] 25 | 26 | def __init__(self): 27 | self._dict = {} 28 | 29 | # parse filename 30 | def read(self, filename): 31 | f = open(filename, "r") 32 | lines = f.readlines() 33 | 34 | if len(lines) == 0: 35 | return 36 | 37 | section = self._dict 38 | idx = 0 39 | while idx < len(lines): 40 | stripped = lines[idx].strip() 41 | 42 | # ignore comments 43 | if stripped=="" or stripped[0] == ";" or stripped[0] == "#": 44 | idx +=1 45 | continue 46 | 47 | # new section? 48 | if stripped[0] == "[": 49 | secname = stripped[1:stripped.find("]")] 50 | if secname in self._dict: 51 | print("Warning: double section", secname) 52 | else: 53 | self._dict[secname] = {} 54 | section = self._dict[secname] 55 | idx +=1 56 | continue 57 | 58 | # split line in three parts: key, separator, value 59 | for sep in self.seps: 60 | part = lines[idx].partition(sep) 61 | if part[1] == sep: 62 | break 63 | if part[1] != sep: 64 | print("no separator found in line", idx, "\"%s\"" % (lines[idx])) 65 | return 66 | 67 | # either create new value list or add to existing list 68 | if not part[0] in section: 69 | section[part[0]] = [MyRulesValue(part[1], part[2].strip())] 70 | else: 71 | section[part[0]].append(MyRulesValue(part[1], part[2].strip())) 72 | 73 | idx += 1 74 | 75 | f.close() 76 | 77 | def sections(self): 78 | return [k for k,v in self._dict.items() if type(v) == dict] 79 | 80 | def items(self, section): 81 | return [(k,v) for k,v in self._dict[section].items()] 82 | 83 | def has_option(self, section, key): 84 | return key in self._dict[section] 85 | 86 | def has_section(self, section): 87 | return (section in self._dict) and (type(self._dict[section]) == dict) 88 | 89 | def get(self, section, key): 90 | return self._dict[section][key] 91 | 92 | # test if "test" matches one of the values 93 | def match(self, values, test): 94 | val = self.getmatch(values, test) 95 | return val != None 96 | 97 | def getmatch(self, values, test, prefix=""): 98 | for v in values: 99 | #expression = "\"%s\"" % v + seper + "\"%s\"" %test 100 | #return eval(expression) 101 | 102 | if v.sep == "=" and prefix + str(v) == str(test): 103 | return v 104 | if v.sep == "!=" and prefix + str(v) != str(test): 105 | return v 106 | if v.sep == "*=" and re.search(prefix + str(v), str(test)) != None: 107 | return v 108 | return None 109 | 110 | if __name__ == '__main__': 111 | parser = MyRulesParser() 112 | parser.read("action.d/02-test.rules") 113 | 114 | import pprint 115 | pprint.pprint(parser._dict) 116 | 117 | print(parser.sections()) -------------------------------------------------------------------------------- /mplugd/plugins/pi_portmidi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # MIDI plugin - react on MIDI input and send commands to MIDI devices 8 | # 9 | 10 | from __future__ import absolute_import 11 | from __future__ import print_function 12 | import pyportmidi as pm 13 | import time, threading, sys, re 14 | from six.moves import range 15 | 16 | if __name__ == "__main__": 17 | sys.path.append("../") 18 | 19 | from header import MP_Event,MP_object 20 | 21 | mplugd=None 22 | eventloop=None 23 | keywords = ["portmidi"] 24 | outputs=[] 25 | inputs=[] 26 | 27 | class Midi_object(MP_object): 28 | keys = ["name", "cmd", "device"] 29 | 30 | def __init__(self, cmd, device): 31 | MP_object.__init__(self, None, None); 32 | self.cmd = cmd[0] 33 | self.time = cmd[1] 34 | self.device = device 35 | 36 | def __getattr__(self, attr): 37 | if attr == "name": 38 | return self.device["name"] 39 | 40 | if attr == "command": 41 | return "%x" % self.cmd[0] 42 | 43 | if attr == "values": 44 | return " ".join([ "%x" % c for c in self.cmd[1:]]) 45 | 46 | return MP_object.__getattr__(self, attr) 47 | 48 | class PM_Event(MP_Event): 49 | def __init__(self, eventloop, device, event): 50 | title = "MidiEvent" 51 | self.event = event 52 | if event[0][0] == 0x90: 53 | title = "MidiNoteOn" 54 | if event[0][0] == 0xb0: 55 | title = "MidiController" 56 | 57 | super(PM_Event, self).__init__(title) 58 | self.eventloop = eventloop 59 | self.item = Midi_object(event, device); 60 | 61 | def __str__(self): 62 | return "" %(self.__class__.__name__, self.etype, self.event[0]) 63 | 64 | class PM_event_loop(threading.Thread): 65 | def __init__(self, queue): 66 | global inputs 67 | global outputs 68 | 69 | self.queue = queue 70 | self.stop = False 71 | self.initflag = threading.Event() 72 | threading.Thread.__init__(self) 73 | 74 | pm.init() 75 | 76 | for i in range(0,pm.get_count()): 77 | dev = {} 78 | dev["info"] = pm.get_device_info(i) 79 | dev["name"] = dev["info"][1] 80 | dev["input"] = dev["info"][2] 81 | 82 | if dev["input"] == 1: 83 | dev["obj"] = pm.Input(i) 84 | inputs.append(dev) 85 | else: 86 | dev["obj"] = pm.Output(i) 87 | outputs.append(dev) 88 | 89 | self.initflag.set() 90 | 91 | def run(self): 92 | while not self.stop: 93 | for i in inputs: 94 | dev = i["obj"] 95 | while dev.poll(): 96 | cmds = dev.read(5); 97 | if len(cmds) > 0: 98 | if self.queue: 99 | for c in cmds: 100 | pm_ev = PM_Event(self, i, c) 101 | self.queue.push(pm_ev) 102 | else: 103 | print(cmds) 104 | try: 105 | time.sleep(0.1) 106 | except: 107 | break 108 | 109 | def handle_rule_cmd(sparser, pl, val, state, event): 110 | if pl[1] != "portmidi": 111 | return 112 | 113 | if pl[2] == "send": 114 | for o in outputs: 115 | if o["name"] == pl[3]: 116 | # convert 3 hex strings into integers and send to midi device 117 | res = re.search("([\d\w]+)\s+([\d\w]+)\s+([\d\w]+)", val[0]) 118 | if res: 119 | l=[0,0,0] 120 | for i in range(1,4): 121 | l[i-1] = int(res.group(i), 16) 122 | 123 | if mplugd.verbose: 124 | print("MIDI sending %x %x %x" % (l[0], l[1], l[2])) 125 | o["obj"].write_short(l[0], l[1], l[2]); 126 | else: 127 | print("unknown MIDI command: %s" % val) 128 | 129 | def initialize(main,queue): 130 | global mplugd 131 | global eventloop 132 | 133 | mplugd = main 134 | eventloop = PM_event_loop(queue) 135 | 136 | return eventloop 137 | 138 | def shutdown(): 139 | eventloop.stop = True 140 | eventloop.join() 141 | 142 | # causes segfault !? 143 | #for i in inputs: 144 | #i["obj"].close() 145 | #for o in outputs: 146 | #o["obj"].close() 147 | pm.quit() 148 | 149 | def join(): 150 | eventloop.join() 151 | 152 | def get_state(state): 153 | #global eventloop 154 | 155 | #eventloop = PM_event_loop(None) 156 | 157 | #state["portmidi"] = inputs 158 | state["portmidi"] = outputs 159 | 160 | def dump_state(state): 161 | print("PortMIDI\n") 162 | 163 | print("Inputs:") 164 | for i in inputs: 165 | print("\t",i["name"]) 166 | 167 | print("Outputs:") 168 | for i in outputs: 169 | print("\t",i["name"]) 170 | 171 | if __name__ == "__main__": 172 | eventloop = PM_event_loop(None) 173 | eventloop.start() 174 | 175 | while True: 176 | try: 177 | time.sleep(1) 178 | except KeyboardInterrupt: 179 | shutdown() 180 | sys.exit(0) -------------------------------------------------------------------------------- /mplugd/header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # Some common code for mplugd 8 | # 9 | 10 | from __future__ import absolute_import 11 | from __future__ import print_function 12 | import threading, re, os 13 | from pprint import pprint, pformat 14 | from six.moves import range 15 | 16 | mplugd = None 17 | default_config = { 18 | "configfiles": ["/etc/mplugd/mplugd.conf", "~/.mplugd/mplugd.conf", os.path.dirname(__file__)+"/mplugd.conf"], 19 | "plugin_directory": [os.path.dirname(__file__)+"/plugins", "/etc/mplugd/plugins", "~/.mplugd/plugins"], 20 | "action_directory": [os.path.dirname(__file__)+"/action.d", "/etc/mplugd/action.d", "~/.mplugd/action.d"], 21 | "verbose": False 22 | } 23 | 24 | # main class for mplugd 25 | class MPlugD(object): 26 | def __init__(self): 27 | self.config = default_config 28 | self.laststate = None 29 | 30 | for conf in default_config["configfiles"]: 31 | self.read_config(conf) 32 | 33 | # read a INI-like config file 34 | def read_config(self, configfile): 35 | configfile = os.path.expanduser(configfile) 36 | if os.path.exists(configfile): 37 | if self.verbose: 38 | print("Loading config file %s" % configfile) 39 | 40 | f = open(configfile) 41 | lines = f.readlines(); 42 | for line in lines: 43 | res = re.match(r"\s*([\w\.]+)\s*([\+]*=)\s*(.*)\s*", line); 44 | if res: 45 | key = res.group(1); 46 | op = res.group(2) 47 | value = res.group(3); 48 | 49 | key = key.split("."); 50 | k = -1; 51 | itr = self.config; 52 | for k in range(0,len(key)-1): 53 | if not key[k] in itr: 54 | itr[key[k]] = {}; 55 | itr = itr[key[k]]; 56 | if op == "+=" and key[k+1] in itr: 57 | #if not value.strip() in itr[key[k+1]]: 58 | itr[key[k+1]].append(value.strip()); 59 | else: 60 | itr[key[k+1]] = value.strip(); 61 | f.close(); 62 | 63 | def __getattr__(self, attr): 64 | if attr in self.config: 65 | if attr == "verbose": 66 | return self.config[attr] == "1" or self.config[attr] == "True" 67 | else: 68 | return self.config[attr] 69 | else: 70 | raise AttributeError 71 | 72 | # root class for events 73 | class MP_Event(object): 74 | def __init__(self, etype): 75 | self.eventloop = None # the event loop who created this event 76 | self.item = None # the item which this event is about 77 | self.etype = etype # the type of event 78 | 79 | # sometimes we get events that we're not interested in, this flag 80 | # allows to "drop" a event during processing 81 | self.ignore = False 82 | 83 | def __str__(self): 84 | return "" %(self.__class__.__name__, self.etype) 85 | 86 | # the main event queue, handles synchronization itself 87 | class EventQueue(object): 88 | def __init__(self): 89 | self.items = [] 90 | self.cond = threading.Condition() 91 | self.dostop = False 92 | 93 | def stop(self): 94 | self.dostop = True 95 | 96 | def push(self, obj): 97 | if self.dostop: 98 | return 99 | 100 | self.cond.acquire() 101 | self.items.append(obj) 102 | self.cond.notify() 103 | self.cond.release() 104 | 105 | def pop(self): 106 | self.cond.acquire() 107 | while len(self.items) == 0 and not self.dostop: 108 | self.cond.wait(1) 109 | 110 | if len(self.items) == 0 and self.dostop: 111 | self.cond.release() 112 | return 113 | 114 | val = self.items.pop(0) 115 | self.cond.release() 116 | 117 | return val 118 | 119 | # Parent class for all internal representations of PA objects 120 | class MP_object(object): 121 | keys = [] 122 | 123 | def __init__(self, obj, get_attr): 124 | self._obj = obj 125 | self.get_attr = get_attr 126 | 127 | # store object attributes locally (required after the remote PA object vanished) 128 | def cache_obj(self): 129 | for k in self.keys: 130 | setattr(self, k.lower(), getattr(self, k)) 131 | 132 | def __getattr__(self, attr): 133 | # check if requested attribute is part of a sub-object 134 | if attr.find(".") > -1: 135 | a = attr.split(".") 136 | obj = self 137 | for i in range(0, len(a)): 138 | if hasattr(obj, a[i]): 139 | obj = getattr(obj, a[i]) 140 | else: 141 | raise AttributeError("%r object has no attribute %r" % (type(self).__name__, attr)) 142 | return obj; 143 | raise AttributeError("%r object has no attribute %r" % (type(self).__name__, attr)) 144 | 145 | # query PA over dbus if it knows the attribute 146 | try: 147 | val = self.get_attr(self._obj, attr) 148 | except AttributeError: 149 | raise AttributeError("%r object has no attribute %r" % (type(self).__name__, attr)) 150 | if val != None: 151 | return val 152 | 153 | raise AttributeError("%r object has no attribute %r" % (type(self).__name__, attr)) 154 | 155 | # __str__ helper 156 | def getrepr(self): 157 | lst = {} 158 | 159 | for k in self.keys: 160 | if hasattr(self, k): 161 | lst[k] = getattr(self, k) 162 | elif hasattr(self, k.lower()): 163 | lst[k.lower()] = getattr(self, k.lower()) 164 | 165 | return lst 166 | 167 | def __str__(self): 168 | return pformat(self.getrepr(), indent=5) 169 | -------------------------------------------------------------------------------- /mplugd/plugins/pi_udev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # Plugin that listens on udev events 8 | # 9 | 10 | from __future__ import absolute_import 11 | from __future__ import print_function 12 | import pyudev, sys, threading 13 | from pprint import pprint, pformat 14 | 15 | if __name__ == "__main__": 16 | sys.path.append("../") 17 | 18 | from header import MP_Event, MP_object 19 | 20 | mplugd = None 21 | keywords = ["udev"] 22 | eventloop = None 23 | 24 | class Udev_object(MP_object): 25 | keys = ["name", "subsystem", "device_type", "sys_path"] 26 | 27 | def __init__(self, obj): 28 | MP_object.__init__(self, obj, self.get_attr); 29 | 30 | def get_attr(self, obj, attr): 31 | return None 32 | 33 | def __getattr__(self, attr): 34 | if attr == "name": 35 | return self._obj.sys_name 36 | 37 | if attr in list(self._obj.keys()): 38 | return self._obj[attr] 39 | if attr.upper() in list(self._obj.keys()): 40 | return self._obj[attr.upper()] 41 | 42 | if hasattr(self._obj, attr): 43 | return getattr(self._obj, attr) 44 | 45 | if attr in list(self._obj.attributes.keys()): 46 | return self._obj.attributes[attr] 47 | 48 | return MP_object.__getattr__(self, attr) 49 | 50 | def verbose_str(self): 51 | rep = self.getrepr() 52 | 53 | for attr in self._obj.keys(): 54 | rep[attr.lower()] = self._obj[attr] 55 | 56 | for attr in self._obj.attributes.keys(): 57 | try: 58 | rep[attr] = self._obj.attributes[attr] 59 | except KeyError: 60 | # somehow it contains keys that don't exist o.O 61 | pass 62 | 63 | return pformat(rep, indent=5) 64 | 65 | class Udev_Event(MP_Event): 66 | def __init__(self, eventloop, action, device): 67 | super(Udev_Event, self).__init__("Udev%s%s" % (action[0].upper(),action[1:])) 68 | self.eventloop = eventloop 69 | self.item = self.get_item(device) 70 | 71 | def get_item(self, device): 72 | return Udev_object(device) 73 | 74 | def __str__(self): 75 | return "" %(self.__class__.__name__, self.etype, self.item.name) 76 | 77 | # main event loop thread of this plugin 78 | class Udev_event_loop(object): 79 | def __init__(self, queue): 80 | self.queue = queue 81 | self.stop = False 82 | self.initflag = threading.Event() 83 | self.handler = self.event_handler 84 | 85 | def event_handler(self, action, device): 86 | #print action, device 87 | self.queue.push(Udev_Event(self, action, device)) 88 | 89 | def start(self): 90 | self.context = pyudev.Context() 91 | 92 | self.monitor = pyudev.Monitor.from_netlink(self.context) 93 | 94 | self.observer = pyudev.MonitorObserver(self.monitor, self.handler) 95 | self.observer.start() 96 | 97 | self.initflag.set() 98 | 99 | def handle_rule_condition(sparser, pl, values, state, event): 100 | if pl[2] == "device": 101 | #if_udev_device_block_name=dm-0 102 | # 0 1 2 3 4 103 | 104 | for device in eventloop.context.list_devices(subsystem=pl[3]): 105 | dev = Udev_object(device) 106 | #print device 107 | if dev.name in values: 108 | return (False, True) 109 | if mplugd.verbose: 110 | print("no such device") 111 | return (False, False) 112 | 113 | # (ignore, valid) 114 | return (True, False) 115 | 116 | def get_state(state): 117 | pass 118 | 119 | def shutdown(): 120 | eventloop.observer.stop() 121 | 122 | def join(): 123 | pass 124 | 125 | def initialize(main,queue): 126 | global mplugd 127 | global eventloop 128 | 129 | mplugd = main 130 | 131 | eventloop = Udev_event_loop(queue) 132 | 133 | return eventloop 134 | 135 | def dump_state(state): 136 | if "output" in state: 137 | print("Udev") 138 | print("") 139 | print("Please execute \"pi_udev.py list\" for a complete list of devices.") 140 | 141 | if __name__ == "__main__": 142 | if len(sys.argv) == 1: 143 | print("Usage: ", sys.argv[0], "") 144 | sys.exit(0) 145 | 146 | def event_handler(action, device): 147 | #print "Event: ", action, device 148 | e = Udev_Event(eventloop, action, device) 149 | print("Udev%s%s" % (action[0].upper(),action[1:]), e.item.subsystem, e.item.name, e.item.verbose_str()) 150 | 151 | eventloop = Udev_event_loop(None) 152 | eventloop.handler = event_handler 153 | 154 | # workaround 155 | mplugd = eventloop 156 | mplugd.verbose = False 157 | 158 | eventloop.start() 159 | eventloop.initflag.wait() 160 | 161 | if len(sys.argv) == 2 and sys.argv[1] == "list": 162 | for device in eventloop.context.list_devices(): 163 | print(device) 164 | shutdown() 165 | sys.exit(0) 166 | 167 | if len(sys.argv) == 3 and sys.argv[1] == "list": 168 | dev = None 169 | try: 170 | dev = pyudev.Device.from_path(eventloop.context, sys.argv[2]) 171 | except AttributeError: 172 | pass 173 | except pyudev.device.DeviceNotFoundAtPathError: 174 | pass 175 | except: 176 | print("Unexpected error:", sys.exc_info()[0]) 177 | 178 | try: 179 | dev = pyudev.Device.from_device_file(eventloop.context, sys.argv[2]) 180 | except ValueError: 181 | pass 182 | except: 183 | print("Unexpected error:", sys.exc_info()[0]) 184 | 185 | if dev == None: 186 | print(sys.argv[2], "not found") 187 | shutdown() 188 | sys.exit(0) 189 | 190 | print(Udev_object(dev).verbose_str()) 191 | 192 | if sys.argv[1] == "listen": 193 | print("Waiting for udev events...") 194 | import time 195 | while True: 196 | try: 197 | time.sleep(1) 198 | except KeyboardInterrupt: 199 | shutdown() 200 | sys.exit(0) -------------------------------------------------------------------------------- /examples/action.d/02-example.rules: -------------------------------------------------------------------------------- 1 | 2 | ; mplugd rules example 3 | ; ---------------------- 4 | 5 | 6 | ; Example: echo message at startup 7 | [rule startup] 8 | on_type=Startup 9 | true_exec=echo "We're starting!" 10 | 11 | 12 | ; Example: echo message before shutdown 13 | [rule shutdown] 14 | on_type=Shutdown 15 | true_exec=echo "We're stopping!" 16 | 17 | ; Example: if an active port on a sink changes to "analog-output-speaker", 18 | ; print a message to the console 19 | [rule PA_port_event] 20 | on_type=ActivePortUpdated 21 | on_name=analog-output-speaker 22 | true_exec=echo "switch to \"%event_description%\" on device \"%event_device.name%\"" 23 | 24 | ; Example: if DP-0 gets connected to a display with ID "SAMC906", change 25 | ; default sink and all streams from class "musicplayers" to "HDA NVidia" 26 | ; and set DP-0 configuration to automatic 27 | [rule on_dp0_connect] 28 | on_type=OutputChangeNotify 29 | on_name=DP-0 30 | on_connected=1 31 | ; if crtc != 0, the output was already configured. E.g., after executing 32 | ; the xrandr command, a second event occurs to indicate the output is now 33 | ; part of the crtc 34 | on_crtc=0 35 | on_id_string=SAMC906 36 | 37 | true_stream_set_defaultsink_to_alsa.card_name=HDA NVidia 38 | true_stream_move_class_musicplayers_to_alsa.card_name=HDA NVidia 39 | true_exec=xrandr --output DP-0 --auto 40 | 41 | 42 | ; Example: if DP-0 gets disconnected from a display with ID "SAMC906", change 43 | ; default sink and all stream from class "musicplayers" to "HDA Intel PCH" 44 | ; and set DP-0 configuration to off 45 | ; 46 | ; != negates a comparison 47 | [rule on_dp0_disconnect] 48 | on_type=OutputChangeNotify 49 | on_name=DP-0 50 | on_connected=0 51 | ; if crtc == 0, the output was already deactivated 52 | on_crtc!=0 53 | on_id_string=SAMC906 54 | 55 | true_stream_set_defaultsink_to_alsa.card_name=HDA Intel PCH 56 | true_stream_move_class_musicplayers_to_alsa.card_name=HDA Intel PCH 57 | true_exec=xrandr --output DP-0 --off 58 | 59 | ; Please note: there are other tools like mixers or PA itself that restore 60 | ; the output sink for a process. If such tools are active, setting the 61 | ; global default sink has no effect. With mplugd we have two options, either 62 | ; we wait for new streams and set the output sink with additional rules or 63 | ; we modify the restore database of PA. The key value of the database is not 64 | ; fixed, so one has to make sure to use the right parameter in the stream 65 | ; class. You can execute "plugins/pi_pulseaudio.py" directly the get a dump 66 | ; of the current restore database. We recommend to set the sink using 67 | ; additional rules, e.g., see to following two sections. 68 | 69 | ; Example: if a process with binary mplayer starts audio output, move this 70 | ; stream to sink "HDA NVidia" but only if output DP-0 is connected 71 | [rule move_new_mplayer_to_nvidia] 72 | on_type=NewPlaybackStream 73 | on_application.process.binary=mplayer 74 | if_output_DP-0_connected=1 75 | true_stream_move_event_to_alsa.card_name=HDA NVidia 76 | true_exec=echo "moving mplayer to HDA NVidia" 77 | 78 | 79 | ; Example: if a process with binary mplayer starts audio output, move this 80 | ; stream to sink "HDA NVidia" but only if output DP-0 is disconnected 81 | [rule move_new_mplayer_to_intel] 82 | on_type=NewPlaybackStream 83 | on_application.process.binary=mplayer 84 | if_output_DP-0_connected=0 85 | true_stream_move_event_to_alsa.card_name=HDA Intel PCH 86 | true_exec=echo "moving mplayer to HDA Intel PCH" 87 | 88 | ; Example: if a process with binary mplayer starts audio output, move this 89 | ; stream to sink "HDA NVidia" but only if output DP-0 is connected 90 | ; and if the laptop's lid is closed 91 | [rule move_new_mplayer_to_nvidia] 92 | on_type=NewPlaybackStream 93 | on_application.process.binary=mplayer 94 | if_exec=grep -rhl closed /proc/acpi/button/lid/LID/state > /dev/null 95 | true_stream_move_event_to_alsa.card_name=HDA NVidia 96 | true_exec=echo "moving mplayer to HDA NVidia" 97 | 98 | ; Example: echo a message if a music player from class "musicplayers" starts 99 | [rule match_class_of_streams] 100 | on_type=NewPlaybackStream 101 | on_stream_class=musicplayers 102 | true_exec=echo "a music player started to play" 103 | 104 | ; Example: stream class "musicplayers" stands for all streams that belong to a 105 | ; process with a binary named "mplayer", "vlc" or "*player" 106 | ; 107 | ; *= allows regexp matching 108 | [stream_class musicplayers] 109 | stream_application.process.binary=mplayer 110 | stream_application.process.binary=vlc 111 | stream_application.process.binary*=player 112 | ; 113 | ; To automatically set the value in PA's stream restore database, one has 114 | ; to use the matching attribute here. See the output of 115 | ; "plugins/pi_pulseaudio.py" for your current database. E.g., for the 116 | ; application.name attribute it makes a difference if the application uses 117 | ; PA directly or through ALSA (App -> ALSA -> PulseAudio -> ALSA -> Hardware). 118 | ; 119 | stream_application.name=ALSA plug-in [mplayer] 120 | stream_application.name=ALSA plug-in [vlc] 121 | stream_application.id=org.VideoLAN.VLC 122 | 123 | ; Example: if a port is changed in PulseAudio, write a message to the console 124 | [rule PA_port_event] 125 | on_type=ActivePortUpdated 126 | true_exec=echo "switch to \"%event_description%\" on device \"%event_device.name%\"" 127 | 128 | ; Example: if a device with a name matching dvb[0-9].frontend[0-9] appears and 129 | ; if block device "sdc" is present, start recording with mplayer 130 | [rule udev_example] 131 | on_type=UdevAdd 132 | on_name*=dvb[0-9].frontend[0-9] 133 | # is there a device with name "sdc" in subsystem "block"? 134 | if_udev_device_block_name=sdc 135 | true_exec=echo "%event_type% device: %event_name%, starting record" 136 | # start a thread that executes this command 137 | true_exec_thread=mplayer dvb:// -dumpstream -dumpfile /mnt/sda/dump.ts -------------------------------------------------------------------------------- /mplugd/plugins/pi_xevents.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # Plugin that listens for X events 8 | # 9 | 10 | from __future__ import absolute_import 11 | from __future__ import print_function 12 | import Xlib, Xlib.display, Xlib.ext.randr as randr 13 | import edid, threading 14 | from pprint import pprint, pformat 15 | import header 16 | from six.moves import map 17 | from six.moves import range 18 | 19 | mplugd = None 20 | keywords = ["output"] 21 | eventloop = None 22 | 23 | # internal representation of a X output device 24 | class Output(object): 25 | def __init__(self): 26 | self._data = None 27 | self._edid = None 28 | 29 | def __getattr__(self, attr): 30 | # connection = 0 -> connected = 1, connection=2 -> unknown 31 | if attr == "connected": 32 | if self._data.connection == 2: 33 | return 2 34 | else: 35 | return 1 - self._data.connection 36 | 37 | if hasattr(self._data, attr): 38 | return getattr(self._data, attr) 39 | if self._edid and hasattr(self._edid, attr): 40 | return getattr(self._edid, attr) 41 | return "" 42 | 43 | def __str__(self): 44 | lst = {} 45 | 46 | # keys we will show during a dump 47 | keys = ["name", "connected", "model_name", "id_string","crtc", 48 | "mm_width","mm_height","subpixel_order","crtcs", 49 | "modes", "clones", "serial_number", "date", "video_input_def", 50 | "size", "gamma", "feature_support", "color_characteristics", 51 | "timings", "range_dt", "timing_dt", "monitor_details"] 52 | 53 | for k in keys: 54 | lst[k] = getattr(self, k) 55 | 56 | return pformat(lst, indent=5) 57 | 58 | # query the edid module for output_nr 59 | def get_edid(display, output_nr): 60 | PROPERTY_EDID = 76 61 | INT_TYPE = 19 62 | 63 | props = display.xrandr_list_output_properties(output_nr) 64 | if PROPERTY_EDID in props.atoms: 65 | try: 66 | rawedid = display.xrandr_get_output_property(output_nr, PROPERTY_EDID, INT_TYPE, 0, 400) 67 | except: 68 | print("error loading EDID data of output", output_nr) 69 | return None 70 | edidstream = rawedid._data['value'] 71 | e2 = ''.join(map(chr, edidstream)) 72 | e = edid.Edid(e2) 73 | 74 | return e 75 | else: 76 | return None 77 | 78 | # X event class 79 | class XMP_Event(header.MP_Event): 80 | def __init__(self, eventloop, event): 81 | super(XMP_Event, self).__init__(event.__class__.__name__) 82 | self.event = event 83 | self.eventloop = eventloop 84 | self.item = self.get_event_item() 85 | 86 | # process the "raw" event from Xorg and set self.item to the item in question 87 | def get_event_item(self): 88 | if self.etype == "OutputChangeNotify": 89 | # get additional info for the event's output 90 | # required for disconnect events because we cannot query the 91 | # server for info afterwards 92 | newoutputs = get_state_outputs() 93 | if self.event.output in list(newoutputs.keys()): 94 | self.item = newoutputs[self.event.output] 95 | elif self.event.output in list(mplugd.laststate["output"].keys()): 96 | self.item = mplugd.laststate["output"][self.event.output] 97 | else: 98 | print("Did not find information on output", self.event.output) 99 | 100 | if (not self.item._edid or not self.item._edid.valid) and self.event.output in list(mplugd.laststate["output"].keys()): 101 | self.item._edid = mplugd.laststate["output"][self.event.output]._edid 102 | 103 | return self.item 104 | 105 | return None 106 | 107 | # main event loop thread of this plugin 108 | class X_event_loop(threading.Thread): 109 | def __init__(self, queue): 110 | self.queue = queue 111 | self.stop = False 112 | self.initflag = threading.Event() 113 | self.xlock = threading.Lock() 114 | threading.Thread.__init__(self) 115 | 116 | def run(self): 117 | self.xlock.acquire() 118 | # get X objects 119 | self.display = Xlib.display.Display(':0') 120 | display = self.display 121 | self.root = display.screen().root 122 | root = self.root 123 | 124 | if not display.has_extension("RANDR"): 125 | print("RANDR extension not found") 126 | sys.exit(1) 127 | 128 | display.query_extension('RANDR') 129 | if mplugd.verbose: 130 | r = display.xrandr_query_version() 131 | print("RANDR extension version %d.%d" % (r.major_version, r.minor_version)) 132 | 133 | # set which types of events we want to receive 134 | root.xrandr_select_input( 135 | randr.RRScreenChangeNotifyMask 136 | | randr.RRCrtcChangeNotifyMask 137 | | randr.RROutputChangeNotifyMask 138 | | randr.RROutputPropertyNotifyMask 139 | ) 140 | 141 | self.xlock.release() 142 | self.initflag.set() 143 | 144 | import time 145 | # enter event loop 146 | while not self.stop: 147 | for x in range(0, self.root.display.pending_events()): 148 | e = self.root.display.next_event() 149 | mp_ev = XMP_Event(self, e) 150 | self.queue.push(mp_ev) 151 | 152 | # TODO find something better 153 | time.sleep(1) 154 | 155 | ## other randr events 156 | #if e.__class__.__name__ == randr.ScreenChangeNotify.__name__: 157 | #if e.__class__.__name__ == randr.CrtcChangeNotify.__name__: 158 | #if e.__class__.__name__ == randr.OutputPropertyNotify.__name__: 159 | #if e.__class__.__name__ == randr.OutputChangeNotify.__name__: 160 | 161 | # get current list of outputs and create internal Output objects 162 | def get_state_outputs(): 163 | eventloop.xlock.acquire() 164 | resources = eventloop.root.xrandr_get_screen_resources()._data 165 | 166 | dic = {} 167 | for idx in resources["outputs"]: 168 | o = Output() 169 | o._data = eventloop.display.xrandr_get_output_info(idx, resources['config_timestamp']) 170 | o._edid = get_edid(eventloop.display, idx) 171 | 172 | #state["outputs"][o.name] = o 173 | dic[idx] = o 174 | 175 | eventloop.xlock.release() 176 | 177 | return dic 178 | 179 | def get_state(state): 180 | state["output"] = get_state_outputs() 181 | 182 | def dump_state(state): 183 | if "output" in state: 184 | print("Xorg") 185 | print("") 186 | for k,v in state["output"].items(): 187 | print(v.name, "(ID: %s)" % k) 188 | print(str(v)) 189 | 190 | def shutdown(): 191 | eventloop.stop = True 192 | 193 | def join(): 194 | eventloop.join() 195 | 196 | def initialize(main,queue): 197 | global mplugd 198 | global eventloop 199 | 200 | mplugd = main 201 | 202 | if int(Xlib.__version__[0]) == 0 and int(Xlib.__version__[1]) <= 15 and not hasattr(Xlib.display.Display, "extension_add_subevent"): 203 | print("Require at least python-xlib SVN revision > r160 or version > 0.15. Your version:", Xlib.__version_string__) 204 | return None 205 | 206 | eventloop = X_event_loop(queue) 207 | 208 | return eventloop 209 | -------------------------------------------------------------------------------- /mplugd/plugins/edid.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ## Taken from the xac project (http://dev.gentoo.org/~josejx/xac.html) 4 | ## the ebuild says its GPL-2 5 | ## original version 1.2 from: Feb 27 14:47:45 2007 6 | 7 | 8 | ### FIXME recheck endianness for these structs 9 | 10 | from __future__ import absolute_import 11 | from struct import pack, unpack 12 | from six.moves import range 13 | 14 | ### EDID Header Magic 15 | EDID_HEADER = "\0\xFF\xFF\xFF\xFF\xFF\xFF\0" 16 | 17 | ### ASCII capital letter offset 18 | ASC = 64 19 | SERIAL_DT = "\0\0\0\xFF\0" 20 | ASCII_DT = "\0\0\0\xFE\0" 21 | RANGE_DT = "\0\0\0\xFD\0" 22 | NAME_DT = "\0\0\0\xFC\0" 23 | 24 | class Edid(object): 25 | def __init__(self, edid=None): 26 | self.edid = None 27 | self.valid = 1 28 | 29 | ### Parse the passed in edid 30 | if edid == None: 31 | # print "Error: Empty EDID" 32 | self.valid = 0 33 | edid = "\0" * 128 34 | 35 | ### Make sure it a valid edid (header matches) 36 | if edid[:8] != EDID_HEADER: 37 | # print "Error: Invalid EDID" 38 | self.valid = 0 39 | edid = "\0" * 128 40 | 41 | ### Check the checksum 42 | sum = 0 43 | for i in range(128): 44 | sum = (sum + int(unpack('B', edid[i])[0])) & 0xFF 45 | if sum != 0: 46 | # print "Checksum failed, result should be 0, instead it's:", sum 47 | self.valid = 0 48 | edid = "\0" * 128 49 | 50 | self.edid = edid 51 | 52 | def get_id_string(self): 53 | name = unpack('>H',self.edid[8:10])[0] 54 | ### FIXME Be sure to check endianness on an x86 machine 55 | n = chr(((name >> 10) & 0x1F) + ASC) + chr(((name >> 5) & 0x1F) + ASC) + chr(((name >> 0) & 0x1F) + ASC) 56 | return n + hex(unpack('>H', self.edid[10:12])[0])[2:].upper() 57 | 58 | def get_serial_number(self): 59 | return unpack('>I', self.edid[12:16])[0] 60 | 61 | ### Tupple of week, year from EDID 62 | def get_date(self): 63 | date = unpack('BB', self.edid[16:18]) 64 | return [date[0], date[1] + 1990] 65 | 66 | ### EDID version, revision 67 | def get_edid_ver(self): 68 | return unpack('BB', self.edid[18:20]) 69 | 70 | def get_video_input_def(self): 71 | vid_in = {} 72 | v = unpack('B', self.edid[20])[0] 73 | vid_in['digital'] = (v >> 7) & 1 74 | if vid_in['digital']: 75 | vid_in['DFP1x'] = (v & 1) 76 | else: 77 | vid_in['video_level'] = (v >> 5) & 3 78 | vid_in['blank_to_black'] = (v >> 4) & 1 79 | vid_in['seperate_sync'] = (v >> 3) & 1 80 | vid_in['composite_sync'] = (v >> 2) & 1 81 | vid_in['sync_on_green'] = (v >> 1) & 1 82 | vid_in['serration_vsync'] = (v & 1) 83 | return vid_in 84 | 85 | ### Size of screen (horiz, vert) 86 | def get_size(self): 87 | return unpack('BB', self.edid[21:23]) 88 | 89 | def get_gamma(self): 90 | return (unpack('B', self.edid[23])[0] / 100.0) + 1.0 91 | 92 | def get_feature_support(self): 93 | fs = {} 94 | f = unpack('B', self.edid[24])[0] 95 | fs['standby'] = (f >> 7) & 1 96 | fs['suspend'] = (f >> 6) & 1 97 | fs['active_off'] = (f >> 5) & 1 98 | fs['display_type'] = (f >> 3) & 3 99 | fs['rgb'] = (f >> 2) & 1 100 | fs['prefered_timing'] = (f >> 1) & 1 101 | fs['gtf'] = (f & 1) 102 | return fs 103 | 104 | def get_color_characteristics(self): 105 | cc = {} 106 | c = unpack('10B', self.edid[25:35]) 107 | ### Check the / 1024 value... (is this right?) 108 | cc['red_x'] = ((c[2] << 2) + ((c[0] >> 6) & 3)) / 1024.0 109 | cc['red_y'] = ((c[3] << 2) + ((c[0] >> 4) & 3)) / 1024.0 110 | cc['green_x'] = ((c[4] << 2) + ((c[0] >> 2) & 3)) / 1024.0 111 | cc['green_y'] = ((c[5] << 2) + (c[0] & 3)) / 1024.0 112 | cc['blue_x'] = ((c[6] << 2) + ((c[1] >> 6) & 3)) / 1024.0 113 | cc['blue_y'] = ((c[7] << 2) + ((c[1] >> 4) & 3)) / 1024.0 114 | cc['white_x'] = ((c[8] << 2) + ((c[1] >> 2) & 3)) / 1024.0 115 | cc['white_y'] = ((c[9] << 2) + (c[1] & 3)) / 1024.0 116 | return cc 117 | 118 | def get_timings(self): 119 | timing = [ [720,400,70], [720,400,88], [640,480,60], [640,480, 67], 120 | [640,480,72], [640,480,75], [800,600,56], [800,600, 60], 121 | [800,600,72], [800,600,75], [832,624,75], [1024,768, 87], 122 | [1024,768,60], [1024,768,70], [1024,768,75], [1280, 1024,75] ] 123 | my_timing = [] 124 | 125 | ### Established timings 126 | est = unpack('BB', self.edid[35:37]) 127 | 128 | ### This is what the field is set to if not used 129 | if est[0] == "\x01": 130 | est[0] = 0 131 | elif est[1] == "\x01": 132 | est[1] = 0 133 | 134 | for j in range(2): 135 | for i in range(8): 136 | if ((est[j] >> i) & 1): 137 | my_timing.append(timing[i]) 138 | 139 | ### Add the "standard" timings into the timing array 140 | std = unpack('>8H', self.edid[38:54]) 141 | man = unpack('B', self.edid[37])[0] 142 | 143 | for i in range(8): 144 | if ((man >> i) & 1): 145 | horiz = ((std[i] & 0xFF00) >> 8) * 8 + 248 146 | aspect = std[i] & 3 147 | if aspect == 0: 148 | vert = (horiz * 16) / 10 149 | elif aspect == 1: 150 | vert = (horiz * 4) / 3 151 | elif aspect == 2: 152 | vert = (horiz * 5) / 4 153 | else: # aspect == 3 154 | vert = (horiz * 16) / 9 155 | 156 | freq = ((std[i] & 0x00FC) >> 2) + 60 157 | my_timing.append([horiz, vert, freq]) 158 | return my_timing 159 | 160 | ### Monitor Descriptor 161 | def get_range_dt(self, tag): 162 | sync = {} 163 | ### If possible, we get the range limit from the range limit tag 164 | sync['v_min'] = unpack('B',tag[5])[0] 165 | sync['v_max'] = unpack('B',tag[6])[0] 166 | sync['h_min'] = unpack('B',tag[7])[0] 167 | sync['h_max'] = unpack('B',tag[8])[0] 168 | return sync 169 | 170 | ### Detailed Timing (from monitor details) 171 | def get_timing_dt(self, info): 172 | ### Search for detailed timing info 173 | timing = {} 174 | timing['pixel_clock'] = unpack(">H",info[:2])[0] 175 | 176 | timing['horizontal_active'] = unpack("B",info[2])[0] + ((unpack("B",info[4])[0] >> 4) << 8) 177 | timing['horizontal_blanking'] = unpack("B",info[3])[0] + ((unpack("B",info[4])[0] & 0xF) << 8) 178 | 179 | timing['vertical_active'] = unpack("B",info[5])[0] + (((unpack("B",info[7])[0] >> 4) & 0xF) << 8) 180 | timing['vertical_blanking'] = unpack("B",info[6])[0] + ((unpack("B",info[7])[0] & 0xF) << 8) 181 | 182 | timing['hsync_offset'] = unpack("B",info[8])[0] + (((unpack("B", info[11])[0] >> 6) & 3) << 8) 183 | timing['hsync_pulse_width'] = unpack("B",info[9])[0] + (((unpack("B", info[11])[0] >> 4) & 3) << 8) 184 | timing['vsync_offset'] = (unpack("B",info[10])[0] >> 4) + (((unpack("B", info[11])[0] >> 2) & 3) << 8) 185 | timing['vsync_pulse_width'] = (unpack("B",info[10])[0] & 0xF) + ((unpack("B", info[11])[0] & 3) << 8) 186 | 187 | timing['himage_size'] = unpack("B",info[12])[0] + ((unpack("B",info[14])[0] >> 4) << 8) 188 | timing['vimage_size'] = unpack("B",info[13])[0] + ((unpack("B",info[14])[0] & 0xF) << 8) 189 | 190 | timing['hborder'] = unpack("B",info[15])[0] 191 | timing['vborder'] = unpack("B",info[16])[0] 192 | 193 | timing['interlaced'] = (unpack("B", info[17])[0] >> 7) & 1 194 | timing['stereo'] = (unpack("B", info[17])[0] >> 5) & 3 195 | timing['digital_composite'] = (unpack("B", info[17])[0] >> 3) & 3 196 | timing['variant'] = (unpack("B", info[17])[0] >> 1) & 3 197 | return timing 198 | 199 | def get_monitor_details(self): 200 | details = [] 201 | for i in range(4): 202 | dmt_offset = i * 18 + 54 203 | tag = self.edid[dmt_offset:dmt_offset+18] 204 | 205 | if tag[:5] == SERIAL_DT: 206 | for i in range(13): 207 | if tag[i + 5] == "\x0A" or tag[i + 5] == "\x00": 208 | i = i - 1 209 | break 210 | size = str(i + 1) + "s" 211 | details.append(["Serial", unpack(size, tag[5:i + 6])[0]]) 212 | elif tag[:5] == ASCII_DT: 213 | for i in range(13): 214 | if tag[i + 5] == "\x0A" or tag[i + 5] == "\x00": 215 | i = i - 1 216 | break 217 | size = str(i + 1) + "s" 218 | details.append(["ASCII", unpack(size, tag[5:i + 6])[0]]) 219 | elif tag[:5] == RANGE_DT: 220 | details.append(["Range", self.get_range_dt(tag)]) 221 | elif tag[:5] == NAME_DT: 222 | for i in range(13): 223 | if tag[i + 5] == "\x0A" or tag[i + 5] == "\x00": 224 | i = i - 1 225 | break 226 | size = str(i + 1) + "s" 227 | details.append(["Name", unpack(size, tag[5:i + 6])[0]]) 228 | elif tag[:3] == "\0\0\0" and (0 < unpack('B', tag[3])[0] < 16): 229 | details.append(["Manufacturer", str(unpack("13B", tag[5:18]))]) 230 | elif tag[:2] != "\0\0": 231 | details.append(["Detailed Timing", self.get_timing_dt(tag)]) 232 | return details 233 | 234 | ### Get a readable name for this monitor 235 | def get_model_name(self): 236 | s = "" 237 | details = self.get_monitor_details() 238 | for i in details: 239 | if i[0] == "Name" or i[0] == "ASCII": 240 | s = s + i[1] + " " 241 | if not len(s): 242 | s = self.get_id_string() 243 | 244 | return s.strip() 245 | 246 | def has_extension(self): 247 | return unpack("B", self.edid[126])[0] 248 | 249 | def __getattr__(self, attr): 250 | #if hasattr(self, "get_%s" % attr): 251 | return getattr(self, "get_%s" % attr)() 252 | 253 | def __str__(self): 254 | vals = [] 255 | for i in dir(self): 256 | vals.append(getattr(self, i)) 257 | return str(vals) 258 | 259 | -------------------------------------------------------------------------------- /mplugd/mplugd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # This is the main file for mplugd 8 | # 9 | 10 | from __future__ import absolute_import 11 | from __future__ import print_function 12 | import os, argparse, sys, subprocess 13 | 14 | from .header import * 15 | from .rulesparser import * 16 | 17 | 18 | # thread that pops events from queue and processes them 19 | class Event_Consumer(threading.Thread): 20 | def __init__(self, queue): 21 | self.queue = queue 22 | #self.stop = False 23 | threading.Thread.__init__(self) 24 | 25 | def run(self): 26 | while not self.queue.dostop: 27 | # pop blocks 28 | val = self.queue.pop() 29 | if val: 30 | if val.ignore: 31 | if mplugd.verbose: 32 | print("Ignoring event: ", val) 33 | continue 34 | 35 | if mplugd.verbose: 36 | print("Handling event: ", val) 37 | self.handle_event(val) 38 | 39 | def handle_event(self, e): 40 | mplugd.laststate = get_state(); 41 | 42 | for directory in mplugd.config["action_directory"]: 43 | directory = os.path.expanduser(directory) 44 | sys.path.append(directory) 45 | for root, dirs, files in os.walk(directory): 46 | files.sort() 47 | for filename in files: 48 | fullpath = os.path.join(root, filename) 49 | 50 | if filename.split(".")[-1] == "py": 51 | mod = __import__(".".join(filename.split(".")[:-1])) 52 | mod.process(mplugd, e) 53 | 54 | if filename.split(".")[-1] == "rules": 55 | execute_rules(fullpath, e) 56 | 57 | # substitute %variables% in value 58 | def rules_substitute_value(event, value): 59 | import re 60 | 61 | result = "" 62 | res = True 63 | while res: 64 | res = re.search("%([\w\._\s]*)%", value) 65 | if res: 66 | # replace %% with % 67 | if res.group(1) == "": 68 | result = result + value[:res.start()] + "%" 69 | value = value[res.end():] 70 | continue 71 | 72 | sub = "" 73 | vlist = res.group(1).split("_") 74 | 75 | if res.group(1) == "event_type": 76 | sub = event.etype 77 | if vlist[0] == "event" and event.item and hasattr(event.item, "_".join(vlist[1:])): 78 | sub = getattr(event.item, "_".join(vlist[1:])) 79 | if vlist[0] in mplugd.laststate and vlist[1] in mplugd.laststate[vlist[0]] and hasattr(mplugd.laststate[vlist[0]][vlist[1]], "_".join(vlist[2:])): 80 | sub = getattr(mplugd.laststate[vlist[0]][vlist[1]], "_".join(vlist[2:])) 81 | 82 | # result contains the substituted part of value 83 | result = result + value[:res.start()] + str(sub) 84 | # value contains the unprocessed part of the original value 85 | value = value[res.end():] 86 | 87 | # return substituted string and the remaining part of value 88 | return result + value 89 | 90 | class ThreadedCommand(threading.Thread): 91 | def __init__(self, cmd): 92 | self.cmd = cmd 93 | threading.Thread.__init__(self) 94 | 95 | def run(self): 96 | #subprocess.call(self.cmd, shell=True) 97 | # required for mplayer 98 | subprocess.Popen(self.cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 99 | 100 | if mplugd.verbose: 101 | print("thead executing \"%s\" returned." % self.cmd) 102 | 103 | # process rules file from action.d/ 104 | def execute_rules(filename, event): 105 | if mplugd.verbose: 106 | print("Executing rules", filename) 107 | 108 | sparser = MyRulesParser() 109 | sparser.read(filename) 110 | 111 | for s in sparser.sections(): 112 | typ = s.split(" ") 113 | if not typ[0] == "rule": 114 | continue 115 | 116 | if mplugd.verbose: 117 | print("Processing section", s) 118 | 119 | # start itemlist with on_ items 120 | itemlist = [] 121 | for (k,v) in sparser.items(s): 122 | if k.startswith("on_"): 123 | itemlist.append((k,v)) 124 | 125 | for (k,v) in sparser.items(s): 126 | if not k.startswith("on_"): 127 | itemlist.append((k,v)) 128 | 129 | # evaluate all if conditions in this section 130 | execute = True 131 | for (k,values) in itemlist: 132 | # we're only interested in conditions here 133 | if k.split("_")[0] == "true" or k.split("_")[0] == "false": 134 | continue 135 | 136 | if mplugd.verbose: 137 | print("Item: %s %s %s ..." %(k, values[0].sep, values), end=' ') 138 | 139 | if k[:len("if_present_")] == "if_present_": 140 | # if_present_output_name=DP-0 141 | ki = k[len("if_present_"):].split("_") 142 | 143 | found = False 144 | if not ki[0] in mplugd.laststate: 145 | continue 146 | for (idx, obj) in mplugd.laststate[ki[0]].items(): 147 | if sparser.match(values, getattr(obj,"_".join(ki[1:]))): 148 | found = True 149 | break; 150 | if found: 151 | if mplugd.verbose: 152 | print("found") 153 | else: 154 | if mplugd.verbose: 155 | print("not found") 156 | execute = False 157 | break 158 | 159 | elif k == "if_exec": 160 | for cmd in values: 161 | retval = subprocess.call(cmd, shell=True) 162 | if retval: 163 | if mplugd.verbose: 164 | print(cmd, "returned:", retval, "expected 0") 165 | execute = False 166 | break 167 | else: 168 | if mplugd.verbose: 169 | print("match") 170 | 171 | elif k[:len("if_")] == "if_": 172 | # if_output_DP-0_connected=1 173 | 174 | ki = k[len("if_"):].split("_") 175 | 176 | found=False 177 | pl = k.split("_") 178 | for p in plugins: 179 | if pl[1] in p.keywords and hasattr(p, "handle_rule_condition"): 180 | found = True 181 | break 182 | 183 | if found: 184 | ignore, valid = p.handle_rule_condition(sparser, pl, values, mplugd.laststate, event) 185 | if not ignore: 186 | if valid: 187 | if mplugd.verbose: 188 | print(ki[0], "match") 189 | continue 190 | else: 191 | execute = False 192 | break 193 | 194 | if not ki[0] in mplugd.laststate: 195 | if mplugd.verbose: 196 | print(ki[0], "not found") 197 | execute = False 198 | break 199 | 200 | # get output id for output name 201 | outputid = None 202 | for k2,v2 in mplugd.laststate[ki[0]].items(): 203 | if v2.name == ki[1]: 204 | outputid = k2 205 | break 206 | 207 | if outputid: 208 | if not sparser.match(values, str(getattr(mplugd.laststate[ki[0]][outputid], "_".join(ki[2:])))): 209 | execute = False 210 | if mplugd.verbose: 211 | print("mismatch: =\"%s\"" % (getattr(mplugd.laststate[ki[0]][outputid], "_".join(ki[2:])))) 212 | break 213 | else: 214 | if mplugd.verbose: 215 | print("match") 216 | else: 217 | if mplugd.verbose: 218 | print(ki[1], "not found") 219 | execute = False 220 | break 221 | 222 | elif k.startswith("on_type"): 223 | # on_type=NewPlaybackStream 224 | 225 | if not event: 226 | print("no event") 227 | execute = False 228 | break 229 | 230 | if not sparser.match(values, event.etype): 231 | if mplugd.verbose: 232 | print("mismatch", event.etype) 233 | execute = False 234 | break 235 | else: 236 | if mplugd.verbose: 237 | print("match") 238 | 239 | elif k.startswith("on_"): 240 | # on_name*=DP-[0-9] 241 | 242 | if not event: 243 | print("no event") 244 | execute = False 245 | break 246 | 247 | found=False 248 | pl = k.split("_") 249 | for p in plugins: 250 | if pl[1] in p.keywords: 251 | found = True 252 | break 253 | 254 | if found: 255 | if not p.handle_rule_cmd(sparser, pl, values, mplugd.laststate, event): 256 | print("no match") 257 | execute = False 258 | break 259 | else: 260 | pl = k[3:] 261 | 262 | if not hasattr(event.item, pl) or not sparser.match(values, getattr(event.item, pl)): 263 | if mplugd.verbose: 264 | print("mismatch ", end=' ') 265 | if hasattr(event.item, pl): 266 | print(getattr(event.item, pl)) 267 | else: 268 | print("no such attr") 269 | execute = False 270 | break 271 | 272 | if mplugd.verbose: 273 | print("match") 274 | 275 | else: 276 | if mplugd.verbose: 277 | print("skipped") 278 | 279 | # find true/false statements and pass them to the respective plugin 280 | for k,v in sparser.items(s): 281 | pl = k.split("_") 282 | 283 | if not pl[0] == str(execute).lower(): 284 | continue 285 | 286 | if pl[1] == "exec": 287 | if len(pl) == 2: 288 | for cmd in v: 289 | cmd = rules_substitute_value(event, cmd) 290 | if mplugd.verbose: 291 | print("exec", cmd) 292 | subprocess.call(cmd, shell=True) 293 | if len(pl) == 3 and pl[2] == "thread": 294 | for cmd in v: 295 | cmd = rules_substitute_value(event, cmd) 296 | if mplugd.verbose: 297 | print("exec_thread", cmd) 298 | t = ThreadedCommand(cmd) 299 | t.start() 300 | 301 | for p in plugins: 302 | if pl[1] in p.keywords: 303 | p.handle_rule_cmd(sparser, pl, v, mplugd.laststate, event) 304 | break 305 | 306 | 307 | 308 | # we execute true/false commands directly 309 | #cmd = None 310 | #if execute: 311 | #if sparser.has_option(s, "true_exec"): 312 | #cmd = sparser.get(s, "true_exec") 313 | #else: 314 | #if sparser.has_option(s, "false_exec"): 315 | #cmd = sparser.get(s, "false_exec") 316 | #if cmd: 317 | #for c in cmd: 318 | #c = rules_substitute_value(event, c) 319 | #if mplugd.verbose: 320 | #print "exec", c 321 | #subprocess.call(c, shell=True) 322 | 323 | # query plugins for their state 324 | def get_state(): 325 | state = {} 326 | 327 | for plugin in plugins: 328 | plugin.get_state(state) 329 | 330 | return state 331 | 332 | # stop all threads 333 | def shutdown(): 334 | q.stop() 335 | c.join() 336 | 337 | for plugin in plugins: 338 | if mplugd.verbose: 339 | print("Stopping...", plugin.__name__) 340 | plugin.shutdown() 341 | 342 | for plugin in plugins: 343 | plugin.join() 344 | 345 | c.join() 346 | 347 | ################################ 348 | ### main 349 | 350 | def main(): 351 | global plugins 352 | global q 353 | global c 354 | global mplugd 355 | 356 | parser = argparse.ArgumentParser( 357 | description='mplugd is a daemon that listens on events (e.g. xrandr or pulseaudio) \ 358 | and executes user-defined actions on certain events.') 359 | parser.add_argument('-v', dest='verbose', help='verbose output', action='store_true') 360 | parser.add_argument('-d', dest='state_dump', help='only dump state', action='store_true') 361 | 362 | args = parser.parse_args() 363 | 364 | # set configuration 365 | mplugd = MPlugD() 366 | mplugd.verbose = args.verbose 367 | state_dump = args.state_dump 368 | 369 | q = EventQueue() 370 | 371 | # start thread that pops events from queue and processes them 372 | c = Event_Consumer(q) 373 | c.start() 374 | 375 | # initialize plugins 376 | plugins = [] 377 | sys.path.append(os.path.dirname(__file__)) 378 | for directory in mplugd.config["plugin_directory"]: 379 | directory = os.path.expanduser(directory) 380 | sys.path.append(directory) 381 | for root, dirs, files in os.walk(directory): 382 | files.sort() 383 | for filename in files: 384 | fullpath = os.path.join(root, filename) 385 | 386 | if filename[:2] == "pi" and filename.split(".")[-1] == "py": 387 | if mplugd.verbose: 388 | print("starting plugin", filename) 389 | try: 390 | mod = __import__(".".join(filename.split(".")[:-1])) 391 | except ImportError as e: 392 | print("ImportError:", e) 393 | print("Initialization of plugin %s failed" % filename) 394 | continue 395 | 396 | if not hasattr(mod, "initialize"): 397 | print("Initialization of plugin %s failed" % filename) 398 | continue 399 | 400 | # initialize all constructs 401 | if not mod.initialize(mplugd, q): 402 | print("Initialization of plugin %s failed" % filename) 403 | continue 404 | 405 | plugins.append(mod) 406 | 407 | # start event loop thread 408 | mod.eventloop.start() 409 | 410 | # wait until all plugins are initialized 411 | for plugin in plugins: 412 | plugin.eventloop.initflag.wait() 413 | 414 | # store current state so we still have some information about an item 415 | # after it vanished 416 | mplugd.laststate = get_state(); 417 | 418 | # print all we know about the current state and exit 419 | if state_dump: 420 | for plugin in plugins: 421 | if hasattr(plugin, "dump_state"): 422 | print("====================") 423 | plugin.dump_state(mplugd.laststate) 424 | print("") 425 | 426 | shutdown() 427 | sys.exit(0) 428 | 429 | if mplugd.verbose: 430 | print("Processing static rules...") 431 | # process rules that do not depend on an event 432 | q.push(MP_Event("Startup")) 433 | 434 | # loop until we're intercepted 435 | import time 436 | while 1: 437 | try: 438 | time.sleep(1) 439 | except KeyboardInterrupt: 440 | q.push(MP_Event("Shutdown")) 441 | shutdown() 442 | break 443 | 444 | if __name__ == "__main__": 445 | main() 446 | -------------------------------------------------------------------------------- /mplugd/plugins/pi_pulseaudio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Mario Kicherer (http://kicherer.org) 5 | # License: GPL v2 (http://www.gnu.org/licenses/gpl-2.0.txt) 6 | # 7 | # Plugin that listens for PulseAudio events 8 | # 9 | 10 | from __future__ import absolute_import 11 | from __future__ import print_function 12 | import os, sys, dbus, threading 13 | from pprint import pprint 14 | 15 | from dbus.mainloop.glib import DBusGMainLoop 16 | from dbus.glib import init_threads as ginit_threads 17 | from pprint import pprint, pformat 18 | import six 19 | 20 | try: 21 | from gobject import MainLoop as gMainLoop, threads_init as gthreads_init 22 | except ImportError: 23 | from gi.repository import GObject 24 | gMainLoop = GObject.MainLoop 25 | gthreads_init = GObject.threads_init 26 | 27 | if __name__ == "__main__": 28 | sys.path.append("../") 29 | 30 | from header import MP_Event, MP_object 31 | 32 | mplugd = None 33 | keywords = ["pa", "stream", "sink"] 34 | eventloop = None 35 | 36 | # convert byte array to string 37 | def dbus2str(db): 38 | if type(db)==dbus.Struct: 39 | return str(tuple(dbus2str(i) for i in db)) 40 | if type(db)==dbus.Array: 41 | return "".join([dbus2str(i) for i in db]) 42 | if type(db)==dbus.Dictionary: 43 | return dict((dbus2str(key), dbus2str(value)) for key, value in db.items()) 44 | if type(db)==dbus.String: 45 | return db+'' 46 | if type(db)==dbus.UInt32: 47 | return str(db+0) 48 | if type(db)==dbus.Byte: 49 | return chr(db) 50 | if type(db)==dbus.Boolean: 51 | return db==True 52 | if type(db)==dict: 53 | return dict((dbus2str(key), dbus2str(value)) for key, value in db.items()) 54 | return "(%s:%s)" % (type(db), db) 55 | 56 | def dpprint(data, **kwargs): 57 | pprint(dbus2str(data), **kwargs) 58 | 59 | # DBus wrapper class for PA 60 | class PADbusWrapper(object): 61 | def __init__(self, verbose): 62 | self.bus = None 63 | self.verbose = verbose 64 | self.core = None 65 | 66 | # connect to PA's DBUS 67 | def initialize_pa_bus(self): 68 | dbus_addr = False 69 | while not dbus_addr: 70 | try: 71 | dbus_addr = os.environ.get('PULSE_DBUS_SERVER') 72 | if not dbus_addr: 73 | _sbus = dbus.SessionBus() 74 | dbus_addr = _sbus.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1').Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties') 75 | 76 | except dbus.exceptions.DBusException as exception: 77 | #if exception.get_dbus_name() != 'org.freedesktop.DBus.Error.ServiceUnknown': 78 | #raise 79 | print("Could not connect to PA: ", exception) 80 | return 81 | #import subprocess 82 | #subprocess.Popen(['pulseaudio', '--start', '--log-target=syslog'], stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT).wait() 83 | #from time import sleep 84 | #sleep(1) 85 | #dbus_addr = False 86 | 87 | if mplugd.verbose: 88 | print("PA connecting to", dbus_addr) 89 | 90 | try: 91 | self.bus = dbus.connection.Connection(dbus_addr) 92 | except dbus.exceptions.DBusException: 93 | print("Error while connecting to PA daemon over DBUS, maybe you") 94 | print("have to load the module with:") 95 | print(" pacmd load-module module-dbus-protocol") 96 | self.bus = None 97 | return 98 | 99 | # get PA's core object 100 | self.core = self.bus.get_object(object_path='/org/pulseaudio/core1') 101 | # get the stream restore database 102 | self.ext_restore = dbus.Interface( self.bus.get_object(object_path="/org/pulseaudio/stream_restore1"), dbus_interface='org.freedesktop.DBus.Properties' ) 103 | 104 | 105 | def get_ports(self, sink): 106 | return [ (path, dbus.Interface( self.bus.get_object(object_path=path), dbus_interface='org.freedesktop.DBus.Properties' )) for path in sink.Get('org.PulseAudio.Core1.Device', "Ports") ] 107 | 108 | def get_port_attr(self, port, attr): 109 | return dbus2str(port.Get('org.PulseAudio.Core1.DevicePort', attr)) 110 | 111 | def get_sinks(self): 112 | dbus_sinks = ( dbus.Interface( self.bus.get_object(object_path=path), dbus_interface='org.freedesktop.DBus.Properties' ) for path in 113 | self.core.Get('org.PulseAudio.Core1', 'Sinks', dbus_interface='org.freedesktop.DBus.Properties' ) ) 114 | 115 | sinks = {} 116 | for sink in dbus_sinks: 117 | try: 118 | sinks[sink.Get('org.PulseAudio.Core1.Device', 'Name')] = sink 119 | except dbus.exceptions.DBusException: 120 | pass 121 | #sinks = dict((sink.Get('org.PulseAudio.Core1.Device', 'Name'), sink) for sink in sinks) 122 | return sinks 123 | 124 | def get_sink_attr(self, sink, attr): 125 | return dbus2str(sink.Get('org.PulseAudio.Core1.Device', attr)) 126 | 127 | def get_streams(self): 128 | dbus_pstreams = ( dbus.Interface( self.bus.get_object(object_path=path), dbus_interface='org.freedesktop.DBus.Properties' ) for path in 129 | self.core.Get('org.PulseAudio.Core1', 'PlaybackStreams', dbus_interface='org.freedesktop.DBus.Properties' ) ) 130 | 131 | pstreams = {} 132 | for pstream in dbus_pstreams: 133 | try: 134 | pstreams[pstream.Get('org.PulseAudio.Core1.Stream', 'Index')] = pstream 135 | except dbus.exceptions.DBusException: 136 | pass 137 | #pstreams = dict((pstream.Get('org.PulseAudio.Core1.Stream', 'Index'), pstream) for pstream in pstreams) 138 | 139 | return pstreams 140 | 141 | def get_stream_attr(self, stream, attr): 142 | try: 143 | return dbus2str(stream.Get('org.PulseAudio.Core1.Stream', attr)) 144 | except dbus.exceptions.DBusException: 145 | raise AttributeError 146 | 147 | def move_stream2sink(self, stream, sink): 148 | move = stream.get_dbus_method('Move', 'org.PulseAudio.Core1.Stream') 149 | 150 | move(sink) 151 | 152 | def set_fallback_sink(self, sink): 153 | self.core.Set('org.PulseAudio.Core1', 'FallbackSink', sink, signature="ssv") 154 | 155 | def get_restore_entries(self): 156 | dbus_entries = ( dbus.Interface( self.bus.get_object(object_path=path), dbus_interface='org.freedesktop.DBus.Properties' ) for path in 157 | self.ext_restore.Get('org.PulseAudio.Ext.StreamRestore1', 'Entries', dbus_interface='org.freedesktop.DBus.Properties' ) ) 158 | 159 | entries = {} 160 | for entry in dbus_entries: 161 | try: 162 | entries[entry.Get('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Name')] = entry 163 | except dbus.exceptions.DBusException: 164 | pass 165 | #entries = [(entry.Get('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Name'), entry) for entry in entries] 166 | return entries 167 | 168 | def set_restore_entry(self, entry, device): 169 | entry.Set('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Device', device) 170 | #entries = self.get_restore_entries() 171 | 172 | #found = False 173 | #for e in entries: 174 | #ename = e.Get('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Name') 175 | ##edev = e.Get('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Device') 176 | 177 | #if str(ename) == "sink-input-by-application-name:%s" % name or \ 178 | #str(ename) == "sink-input-by-application-name: ALSA plug-in [%s]" % name: 179 | ##e.Set('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Device', "alsa_output.pci-0000_01_00.1.hdmi-stereo") 180 | ##e.Set('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Device', "alsa_output.pci-0000_00_1b.0.analog-stereo") 181 | ##print "set", name, "to", device 182 | #e.Set('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Device', device) 183 | #found = True 184 | 185 | #if not found: 186 | #print "set_restore_entry: no entry found for", name 187 | 188 | def __getattr__(self, attr): 189 | if hasattr(self, "get_%s" % attr): 190 | return getattr(self, "get_%s" % attr)() 191 | 192 | spl = attr.split("_") 193 | if spl[0] == "get" and spl[1] == "sink": 194 | #'PropertyList' 195 | return self.get_sink_attr(spl[2:]) 196 | if spl[0] == "get" and spl[1] == "stream": 197 | #'PropertyList' 198 | return self.get_stream_attr(spl[2:]) 199 | 200 | print("error", attr) 201 | raise AttributeError 202 | 203 | # Pulseaudio event class 204 | class PAMP_Event(MP_Event): 205 | def __init__(self, eventloop, event, sender, path): 206 | super(PAMP_Event, self).__init__(event.get_member()) 207 | self.eventloop = eventloop 208 | self.sender = sender 209 | self.event = event 210 | self.path = path 211 | 212 | self.item = None 213 | self.get_event_item() 214 | 215 | # process the "raw" event from DBUS and set self.item to the item in question 216 | def get_event_item(self): 217 | if self.event.get_member() == "NewSink" or self.event.get_member() == "SinkRemoved": 218 | newsinks = get_state_sinks() 219 | if self.path in list(newsinks.keys()): 220 | self.item = newsinks[self.path] 221 | elif self.path in list(mplugd.laststate["sink"].keys()): 222 | self.item = mplugd.laststate["sink"][self.path] 223 | else: 224 | print("Did not find information on sink", self.path, newsinks) 225 | 226 | if self.event.get_member() == "ActivePortUpdated": 227 | if not str(self.event.get_path()) in mplugd.laststate["sink"]: 228 | # TODO source events, we only handle sinks for now 229 | self.ignore = True 230 | return 231 | 232 | if self.path in mplugd.laststate["sink"][str(self.event.get_path())].ports: 233 | self.item = mplugd.laststate["sink"][str(self.event.get_path())].ports[self.path] 234 | else: 235 | print("unknown port:", self.path) 236 | 237 | if self.event.get_member() == "NewPlaybackStream" or self.event.get_member() == "PlaybackStreamRemoved": 238 | # get additional info for the event's stream 239 | # required for disconnect events because we cannot query the 240 | # server for info afterwards 241 | newstreams = get_state_streams() 242 | if self.path in list(newstreams.keys()): 243 | self.item = newstreams[self.path] 244 | elif self.path in list(mplugd.laststate["stream"].keys()): 245 | self.item = mplugd.laststate["stream"][self.path] 246 | else: 247 | print("Did not find information on stream", self.path, newstreams) 248 | 249 | if self.event.get_member() == "MuteUpdated": 250 | if self.event.get_path() in mplugd.laststate["sink"]: 251 | #mplugd.laststate["sink"][str(self.event.get_path())]["Mute"] = self.path 252 | self.item = mplugd.laststate["sink"][str(self.event.get_path())] 253 | else: 254 | self.ignore = True 255 | return 256 | 257 | def __str__(self): 258 | return "" %(self.__class__.__name__, self.etype, self.event.get_path(), self.path) 259 | 260 | # main event loop thread for this plugin 261 | class PA_event_loop(threading.Thread): 262 | def __init__(self, queue): 263 | self.queue = queue 264 | self.pa_wrapper = None 265 | self.initflag = threading.Event() 266 | self.loop = None 267 | threading.Thread.__init__(self) 268 | 269 | # push event into main queue 270 | def handler(self, path, sender=None, msg=None): 271 | self.queue.push(PAMP_Event(self, msg, sender, path)) 272 | 273 | def init(self): 274 | DBusGMainLoop(set_as_default=True) 275 | 276 | self.pa_wrapper = PADbusWrapper(True) 277 | self.pa_wrapper.initialize_pa_bus() 278 | 279 | if not self.pa_wrapper.bus: 280 | self.initflag.set() 281 | return False 282 | 283 | return True 284 | 285 | def run(self): 286 | # local callback handler 287 | def cb_handler(path=None, sender=None, msg=None): 288 | self.handler(path, sender, msg) 289 | 290 | self.pa_wrapper.bus.add_signal_receiver(cb_handler, message_keyword="msg") 291 | 292 | core1 = self.pa_wrapper.bus.get_object('org.PulseAudio.Core1', '/org/pulseaudio/core1') 293 | 294 | core1.ListenForSignal('org.PulseAudio.Core1.NewPlaybackStream', dbus.Array(signature="o")) 295 | core1.ListenForSignal('org.PulseAudio.Core1.PlaybackStreamRemoved', dbus.Array(signature="o")) 296 | 297 | core1.ListenForSignal('org.PulseAudio.Core1.NewSink', dbus.Array(signature="o")) 298 | core1.ListenForSignal('org.PulseAudio.Core1.SinkRemoved', dbus.Array(signature="o")) 299 | 300 | core1.ListenForSignal('org.PulseAudio.Core1.Device.ActivePortUpdated', dbus.Array(signature="o")) 301 | core1.ListenForSignal('org.PulseAudio.Core1.Device.MuteUpdated', dbus.Array(signature="o")) 302 | 303 | self.loop = gMainLoop() 304 | gthreads_init() 305 | ginit_threads() 306 | 307 | self.initflag.set() 308 | self.loop.run() 309 | 310 | class PA_object(MP_object): 311 | def __init__(self, dbus_obj, pawrapper, get_attr): 312 | MP_object.__init__(self, dbus_obj, get_attr); 313 | self._props = None 314 | self._pawrapper = pawrapper 315 | 316 | def __getattr__(self, attr): 317 | if attr == "name": 318 | return self.Name 319 | 320 | # check if attribute is in the property dictionary 321 | if self._props and attr in self._props: 322 | return self._props[attr][:-1] 323 | 324 | return MP_object.__getattr__(self, attr) 325 | 326 | # __str__ helper 327 | def getrepr(self): 328 | lst = {} 329 | 330 | lst.update(MP_object.getrepr(self)) 331 | 332 | if self._props: 333 | for k,v in self._props.items(): 334 | lst[k] = v[:-1] 335 | return lst 336 | 337 | # internal representation of a sink 338 | class Sink(PA_object): 339 | keys = ["Name", "Driver", "Index", "Volume", 340 | "Mute", "State", "Channels"] 341 | 342 | def __init__(self, dbus_obj, pawrapper): 343 | PA_object.__init__(self, dbus_obj, pawrapper, pawrapper.get_sink_attr); 344 | self._props = self.get_attr(dbus_obj, 'PropertyList') 345 | 346 | # get the list of ports for this device 347 | self._port_objs = eventloop.pa_wrapper.get_ports(self._obj) 348 | self.ports = {} 349 | for path,pobj in self._port_objs: 350 | p = Port(pobj, pawrapper, self) 351 | p.cache_obj() 352 | self.ports[str(pobj.object_path)] = p 353 | 354 | def __str__(self): 355 | lst = PA_object.getrepr(self) 356 | lst["ports"] = {} 357 | for k,v in self.ports.items(): 358 | lst["ports"][k] = v.getrepr() 359 | return pformat(lst, indent=5) 360 | 361 | # internal representation of a stream 362 | class Stream(PA_object): 363 | keys = ["Name", "Driver", "Index", "Volume", 364 | "Mute", "Channels"] 365 | 366 | def __init__(self, dbus_obj, pawrapper): 367 | PA_object.__init__(self, dbus_obj, pawrapper, pawrapper.get_stream_attr); 368 | self._props = self.get_attr(dbus_obj, 'PropertyList') 369 | 370 | def __getattr__(self, attr): 371 | if attr == "name" or attr == "Name": 372 | if "application.name" in self._props: 373 | return self._props["application.name"][:-1] 374 | elif "media.name" in self._props: 375 | return self._props["media.name"][:-1] 376 | elif "device.description" in self._props: 377 | return self._props["device.description"][:-1] 378 | try: 379 | return PA_object.__getattr__(self, attr) 380 | except AttributeError as exception: 381 | print(exception) 382 | return None 383 | 384 | # internal representation of a sink 385 | class Port(PA_object): 386 | keys = ["Name", "Description", "Priority"] 387 | 388 | def __init__(self, dbus_obj, pawrapper, device): 389 | PA_object.__init__(self, dbus_obj, pawrapper, pawrapper.get_port_attr); 390 | self.device = device 391 | 392 | def __getattr__(self, attr): 393 | if attr == "device": 394 | return self.device 395 | 396 | try: 397 | return PA_object.__getattr__(self, attr) 398 | except AttributeError as exception: 399 | print(exception) 400 | return None 401 | 402 | # query PA for a list of sinks 403 | def get_state_sinks(): 404 | dic = {} 405 | 406 | sinks = eventloop.pa_wrapper.get_sinks() 407 | if sinks: 408 | for sink in sinks.keys(): 409 | s = Sink(sinks[sink], eventloop.pa_wrapper) 410 | s.cache_obj() 411 | 412 | dic[sinks[sink].object_path] = s 413 | return dic 414 | 415 | # query PA for a list of streams 416 | def get_state_streams(): 417 | dic = {} 418 | 419 | streams = eventloop.pa_wrapper.get_streams() 420 | if streams: 421 | for stream in streams.keys(): 422 | s = Stream(streams[stream], eventloop.pa_wrapper) 423 | s.cache_obj() 424 | 425 | dic[streams[stream].object_path] = s 426 | return dic 427 | 428 | def get_state(state): 429 | state["sink"] = get_state_sinks() 430 | state["stream"] = get_state_streams() 431 | 432 | def shutdown(): 433 | eventloop.loop.quit() 434 | 435 | def join(): 436 | eventloop.join() 437 | 438 | # process rules that contain plugin-specific code 439 | def handle_rule_cmd(sparser, pl, val, state, event): 440 | if pl[0] == "on" and pl[1] == "stream": 441 | #on_stream_class 442 | # 0 1 2 443 | sc_section = "stream_class %s" % val[0] 444 | if not sparser.has_section(sc_section): 445 | if mplugd.verbose: 446 | print("Section [stream_class %s] not found" % val[0]) 447 | return False 448 | 449 | for k,v in sparser.items(sc_section): 450 | if sparser.match(v, getattr(event.item, k[7:])): 451 | if mplugd.verbose: 452 | print("match") 453 | return True 454 | 455 | elif pl[2] == "set" and pl[3] == "defaultsink": 456 | #true_pa_set_defaultsink_to_alsa.card_name=HDA Intel PCH 457 | # 0 1 2 3 4 5 458 | for k,v in state["sink"].items(): 459 | if sparser.match(val, getattr(v, "_".join(pl[5:]))): 460 | if mplugd.verbose: 461 | print("set sink to", k) 462 | eventloop.pa_wrapper.set_fallback_sink(v._obj); 463 | 464 | elif pl[3] == "class": 465 | #true_stream_move_class_asd_to_alsa.card_name=HDA NVidia 466 | # 0 1 2 3 4 5 6 467 | sc_section = "stream_class %s" % pl[4] 468 | if not sparser.has_section(sc_section): 469 | if mplugd.verbose: 470 | print("Section [stream_class %s] not found" % pl[4]) 471 | return 472 | 473 | for k,v in state["sink"].items(): 474 | if sparser.match(val, getattr(v, "_".join(pl[6:]))): 475 | rentries = eventloop.pa_wrapper.get_restore_entries() 476 | 477 | for option_key,option_val in sparser.items(sc_section): 478 | # set restore entry for new streams 479 | for name, entry in six.iteritems(rentries): 480 | match = sparser.getmatch(option_val, name, "sink-input-by-%s:" % option_key[7:].replace(".", "-")) 481 | if match != None: 482 | if mplugd.verbose: 483 | print("set restore entry of", "\"%s\"" % name, "to", v.name) 484 | eventloop.pa_wrapper.set_restore_entry(entry, v.name) 485 | 486 | # switch running streams 487 | for stream in state["stream"].values(): 488 | if not hasattr(stream, option_key[7:]): 489 | continue 490 | if sparser.match(option_val, getattr(stream, option_key[7:])): 491 | if mplugd.verbose: 492 | print("move", "\"%s\"" % option_val, "to", k) 493 | eventloop.pa_wrapper.move_stream2sink(stream._obj, v._obj) 494 | 495 | elif pl[3] == "event": 496 | #true_stream_move_event_to_alsa.card_name=HDA Intel PCH 497 | # 0 1 2 3 4 5 498 | for k,v in state["sink"].items(): 499 | if sparser.match(val, getattr(v, "_".join(pl[5:]))): 500 | if mplugd.verbose: 501 | print("move \"%s\" to \"%s\"" % ( event.item.Name, k)) 502 | eventloop.pa_wrapper.move_stream2sink(event.item._obj, v._obj) 503 | break 504 | else: 505 | print(__name__, "unknown command", "_".join(pl), val) 506 | 507 | def dump_state(state): 508 | print("PulseAudio:") 509 | 510 | if "sink" in state: 511 | for k,v in state["sink"].items(): 512 | print("") 513 | if hasattr(v, "alsa.card_name"): 514 | print(getattr(v, "alsa.card_name"), end=' ') 515 | print("(ID: %s)" % k) 516 | print(str(v)) 517 | 518 | if "stream" in state: 519 | for k,v in state["stream"].items(): 520 | print("") 521 | print(v.name, "(ID: %s)" % k) 522 | print(str(v)) 523 | 524 | def initialize(main,queue): 525 | global mplugd 526 | global eventloop 527 | 528 | mplugd = main 529 | eventloop = PA_event_loop(queue) 530 | 531 | if not eventloop.init(): 532 | return None 533 | 534 | return eventloop 535 | 536 | if __name__ == "__main__": 537 | def printhandler(path, sender=None, msg=None): 538 | print("Event: ", path, sender, msg) 539 | 540 | eventloop = PA_event_loop(None) 541 | eventloop.handler = printhandler 542 | 543 | # workaround 544 | mplugd = eventloop 545 | mplugd.verbose = False 546 | 547 | if not eventloop.init(): 548 | shutdown() 549 | 550 | eventloop.start() 551 | eventloop.initflag.wait() 552 | 553 | if not eventloop.loop: 554 | sys.exit(1) 555 | 556 | state = {} 557 | get_state(state) 558 | 559 | dump_state(state); 560 | 561 | 562 | ext_restore = dbus.Interface( eventloop.pa_wrapper.bus.get_object(object_path="/org/pulseaudio/stream_restore1"), dbus_interface='org.freedesktop.DBus.Properties' ) 563 | 564 | entries = ( dbus.Interface( eventloop.pa_wrapper.bus.get_object(object_path=path), dbus_interface='org.freedesktop.DBus.Properties' ) for path in 565 | ext_restore.Get('org.PulseAudio.Ext.StreamRestore1', 'Entries', dbus_interface='org.freedesktop.DBus.Properties' ) ) 566 | 567 | for e in entries: 568 | name = e.Get('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Name'), 569 | dev = e.Get('org.PulseAudio.Ext.StreamRestore1.RestoreEntry', 'Device') 570 | 571 | print(str(name[0]), "--", dev) 572 | 573 | shutdown() 574 | 575 | --------------------------------------------------------------------------------