diff --git a/configuration.yaml b/configuration.yaml index 10db989..20b39c0 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -73,6 +73,8 @@ input_boolean: !include input_boolean.yaml input_slider: !include input_slider.yaml +shell_command: !include shell_command.yaml + script: !include_dir_named scripts automation: !include_dir_merge_list automation diff --git a/ext_scripts/alarm_off.py b/ext_scripts/alarm_off.py new file mode 100644 index 0000000..cf7fe63 --- /dev/null +++ b/ext_scripts/alarm_off.py @@ -0,0 +1,10 @@ +import mpd + +client = mpd.MPDClient() +client.connect("192.168.1.62",6600) +client.stop() +client.clear() +client.random(0) +client.consume(0) +client.close() +client.disconnect() diff --git a/ext_scripts/alarm_on.py b/ext_scripts/alarm_on.py new file mode 100644 index 0000000..d943657 --- /dev/null +++ b/ext_scripts/alarm_on.py @@ -0,0 +1,11 @@ +import mpd + +client = mpd.MPDClient() +client.connect("192.168.1.62",6600) +client.clear() +client.load("morning") +client.random(1) +client.consume(1) +client.play() +client.close() +client.disconnect() diff --git a/ext_scripts/mpd.py b/ext_scripts/mpd.py new file mode 100644 index 0000000..c2bb010 --- /dev/null +++ b/ext_scripts/mpd.py @@ -0,0 +1,455 @@ +# python-mpd: Python MPD client library +# Copyright (C) 2008-2010 J. Alexander Treuman +# +# python-mpd is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-mpd is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with python-mpd. If not, see . + +import socket + + +HELLO_PREFIX = "OK MPD " +ERROR_PREFIX = "ACK " +SUCCESS = "OK" +NEXT = "list_OK" + + +class MPDError(Exception): + pass + +class ConnectionError(MPDError): + pass + +class ProtocolError(MPDError): + pass + +class CommandError(MPDError): + pass + +class CommandListError(MPDError): + pass + +class PendingCommandError(MPDError): + pass + +class IteratingError(MPDError): + pass + + +class _NotConnected(object): + def __getattr__(self, attr): + return self._dummy + + def _dummy(*args): + raise ConnectionError("Not connected") + +class MPDClient(object): + def __init__(self): + self.iterate = False + self._reset() + self._commands = { + # Status Commands + "clearerror": self._fetch_nothing, + "currentsong": self._fetch_object, + "idle": self._fetch_list, + "noidle": None, + "status": self._fetch_object, + "stats": self._fetch_object, + # Playback Option Commands + "consume": self._fetch_nothing, + "crossfade": self._fetch_nothing, + "mixrampdb": self._fetch_nothing, + "mixrampdelay": self._fetch_nothing, + "random": self._fetch_nothing, + "repeat": self._fetch_nothing, + "setvol": self._fetch_nothing, + "single": self._fetch_nothing, + "replay_gain_mode": self._fetch_nothing, + "replay_gain_status": self._fetch_item, + "volume": self._fetch_nothing, + # Playback Control Commands + "next": self._fetch_nothing, + "pause": self._fetch_nothing, + "play": self._fetch_nothing, + "playid": self._fetch_nothing, + "previous": self._fetch_nothing, + "seek": self._fetch_nothing, + "seekid": self._fetch_nothing, + "stop": self._fetch_nothing, + # Playlist Commands + "add": self._fetch_nothing, + "addid": self._fetch_item, + "clear": self._fetch_nothing, + "delete": self._fetch_nothing, + "deleteid": self._fetch_nothing, + "move": self._fetch_nothing, + "moveid": self._fetch_nothing, + "playlist": self._fetch_playlist, + "playlistfind": self._fetch_songs, + "playlistid": self._fetch_songs, + "playlistinfo": self._fetch_songs, + "playlistsearch": self._fetch_songs, + "plchanges": self._fetch_songs, + "plchangesposid": self._fetch_changes, + "shuffle": self._fetch_nothing, + "swap": self._fetch_nothing, + "swapid": self._fetch_nothing, + # Stored Playlist Commands + "listplaylist": self._fetch_list, + "listplaylistinfo": self._fetch_songs, + "listplaylists": self._fetch_playlists, + "load": self._fetch_nothing, + "playlistadd": self._fetch_nothing, + "playlistclear": self._fetch_nothing, + "playlistdelete": self._fetch_nothing, + "playlistmove": self._fetch_nothing, + "rename": self._fetch_nothing, + "rm": self._fetch_nothing, + "save": self._fetch_nothing, + # Database Commands + "count": self._fetch_object, + "find": self._fetch_songs, + "findadd": self._fetch_nothing, + "list": self._fetch_list, + "listall": self._fetch_database, + "listallinfo": self._fetch_database, + "lsinfo": self._fetch_database, + "search": self._fetch_songs, + "update": self._fetch_item, + "rescan": self._fetch_item, + # Sticker Commands + "sticker get": self._fetch_item, + "sticker set": self._fetch_nothing, + "sticker delete": self._fetch_nothing, + "sticker list": self._fetch_list, + "sticker find": self._fetch_songs, + # Connection Commands + "close": None, + "kill": None, + "password": self._fetch_nothing, + "ping": self._fetch_nothing, + # Audio Output Commands + "disableoutput": self._fetch_nothing, + "enableoutput": self._fetch_nothing, + "outputs": self._fetch_outputs, + # Reflection Commands + "commands": self._fetch_list, + "notcommands": self._fetch_list, + "tagtypes": self._fetch_list, + "urlhandlers": self._fetch_list, + "decoders": self._fetch_plugins, + } + + def __getattr__(self, attr): + if attr.startswith("send_"): + command = attr.replace("send_", "", 1) + wrapper = self._send + elif attr.startswith("fetch_"): + command = attr.replace("fetch_", "", 1) + wrapper = self._fetch + else: + command = attr + wrapper = self._execute + if command not in self._commands: + command = command.replace("_", " ") + if command not in self._commands: + raise AttributeError("'%s' object has no attribute '%s'" % + (self.__class__.__name__, attr)) + return lambda *args: wrapper(command, args) + + def _send(self, command, args): + if self._command_list is not None: + raise CommandListError("Cannot use send_%s in a command list" % + command.replace(" ", "_")) + self._write_command(command, args) + retval = self._commands[command] + if retval is not None: + self._pending.append(command) + + def _fetch(self, command, args=None): + if self._command_list is not None: + raise CommandListError("Cannot use fetch_%s in a command list" % + command.replace(" ", "_")) + if self._iterating: + raise IteratingError("Cannot use fetch_%s while iterating" % + command.replace(" ", "_")) + if not self._pending: + raise PendingCommandError("No pending commands to fetch") + if self._pending[0] != command: + raise PendingCommandError("'%s' is not the currently " + "pending command" % command) + del self._pending[0] + retval = self._commands[command] + if callable(retval): + return retval() + return retval + + def _execute(self, command, args): + if self._iterating: + raise IteratingError("Cannot execute '%s' while iterating" % + command) + if self._pending: + raise PendingCommandError("Cannot execute '%s' with " + "pending commands" % command) + retval = self._commands[command] + if self._command_list is not None: + if not callable(retval): + raise CommandListError("'%s' not allowed in command list" % + command) + self._write_command(command, args) + self._command_list.append(retval) + else: + self._write_command(command, args) + if callable(retval): + return retval() + return retval + + def _write_line(self, line): + self._wfile.write("%s\n" % line) + self._wfile.flush() + + def _write_command(self, command, args=[]): + parts = [command] + for arg in args: + parts.append('"%s"' % escape(str(arg))) + self._write_line(" ".join(parts)) + + def _read_line(self): + line = self._rfile.readline() + if not line.endswith("\n"): + raise ConnectionError("Connection lost while reading line") + line = line.rstrip("\n") + if line.startswith(ERROR_PREFIX): + error = line[len(ERROR_PREFIX):].strip() + raise CommandError(error) + if self._command_list is not None: + if line == NEXT: + return + if line == SUCCESS: + raise ProtocolError("Got unexpected '%s'" % SUCCESS) + elif line == SUCCESS: + return + return line + + def _read_pair(self, separator): + line = self._read_line() + if line is None: + return + pair = line.split(separator, 1) + if len(pair) < 2: + raise ProtocolError("Could not parse pair: '%s'" % line) + return pair + + def _read_pairs(self, separator=": "): + pair = self._read_pair(separator) + while pair: + yield pair + pair = self._read_pair(separator) + + def _read_list(self): + seen = None + for key, value in self._read_pairs(): + if key != seen: + if seen is not None: + raise ProtocolError("Expected key '%s', got '%s'" % + (seen, key)) + seen = key + yield value + + def _read_playlist(self): + for key, value in self._read_pairs(":"): + yield value + + def _read_objects(self, delimiters=[]): + obj = {} + for key, value in self._read_pairs(): + key = key.lower() + if obj: + if key in delimiters: + yield obj + obj = {} + elif key in obj: + if not isinstance(obj[key], list): + obj[key] = [obj[key], value] + else: + obj[key].append(value) + continue + obj[key] = value + if obj: + yield obj + + def _read_command_list(self): + try: + for retval in self._command_list: + yield retval() + finally: + self._command_list = None + self._fetch_nothing() + + def _iterator_wrapper(self, iterator): + try: + for item in iterator: + yield item + finally: + self._iterating = False + + def _wrap_iterator(self, iterator): + if not self.iterate: + return list(iterator) + self._iterating = True + return self._iterator_wrapper(iterator) + + def _fetch_nothing(self): + line = self._read_line() + if line is not None: + raise ProtocolError("Got unexpected return value: '%s'" % line) + + def _fetch_item(self): + pairs = list(self._read_pairs()) + if len(pairs) != 1: + return + return pairs[0][1] + + def _fetch_list(self): + return self._wrap_iterator(self._read_list()) + + def _fetch_playlist(self): + return self._wrap_iterator(self._read_playlist()) + + def _fetch_object(self): + objs = list(self._read_objects()) + if not objs: + return {} + return objs[0] + + def _fetch_objects(self, delimiters): + return self._wrap_iterator(self._read_objects(delimiters)) + + def _fetch_changes(self): + return self._fetch_objects(["cpos"]) + + def _fetch_songs(self): + return self._fetch_objects(["file"]) + + def _fetch_playlists(self): + return self._fetch_objects(["playlist"]) + + def _fetch_database(self): + return self._fetch_objects(["file", "directory", "playlist"]) + + def _fetch_outputs(self): + return self._fetch_objects(["outputid"]) + + def _fetch_plugins(self): + return self._fetch_objects(["plugin"]) + + def _fetch_command_list(self): + return self._wrap_iterator(self._read_command_list()) + + def _hello(self): + line = self._rfile.readline() + if not line.endswith("\n"): + raise ConnectionError("Connection lost while reading MPD hello") + line = line.rstrip("\n") + if not line.startswith(HELLO_PREFIX): + raise ProtocolError("Got invalid MPD hello: '%s'" % line) + self.mpd_version = line[len(HELLO_PREFIX):].strip() + + def _reset(self): + self.mpd_version = None + self._iterating = False + self._pending = [] + self._command_list = None + self._sock = None + self._rfile = _NotConnected() + self._wfile = _NotConnected() + + def _connect_unix(self, path): + if not hasattr(socket, "AF_UNIX"): + raise ConnectionError("Unix domain sockets not supported " + "on this platform") + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(path) + return sock + + def _connect_tcp(self, host, port): + try: + flags = socket.AI_ADDRCONFIG + except AttributeError: + flags = 0 + err = None + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, socket.IPPROTO_TCP, + flags): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + sock.connect(sa) + return sock + except socket.error, err: + if sock is not None: + sock.close() + if err is not None: + raise err + else: + raise ConnectionError("getaddrinfo returns an empty list") + + def connect(self, host, port): + if self._sock is not None: + raise ConnectionError("Already connected") + if host.startswith("/"): + self._sock = self._connect_unix(host) + else: + self._sock = self._connect_tcp(host, port) + self._rfile = self._sock.makefile("rb") + self._wfile = self._sock.makefile("wb") + try: + self._hello() + except: + self.disconnect() + raise + + def disconnect(self): + self._rfile.close() + self._wfile.close() + self._sock.close() + self._reset() + + def fileno(self): + if self._sock is None: + raise ConnectionError("Not connected") + return self._sock.fileno() + + def command_list_ok_begin(self): + if self._command_list is not None: + raise CommandListError("Already in command list") + if self._iterating: + raise IteratingError("Cannot begin command list while iterating") + if self._pending: + raise PendingCommandError("Cannot begin command list " + "with pending commands") + self._write_command("command_list_ok_begin") + self._command_list = [] + + def command_list_end(self): + if self._command_list is None: + raise CommandListError("Not in command list") + if self._iterating: + raise IteratingError("Already iterating over a command list") + self._write_command("command_list_end") + return self._fetch_command_list() + + +def escape(text): + return text.replace("\\", "\\\\").replace('"', '\\"') + diff --git a/ext_scripts/mpd.pyc b/ext_scripts/mpd.pyc new file mode 100644 index 0000000..29aa73b Binary files /dev/null and b/ext_scripts/mpd.pyc differ diff --git a/shell_command.yaml b/shell_command.yaml new file mode 100644 index 0000000..130b2ef --- /dev/null +++ b/shell_command.yaml @@ -0,0 +1,2 @@ +alarm_off: python2 /config/ext_scripts/alarm_off.py +alarm_on: python2 /config/ext_scripts/alarm_on.py