From f6e3b40b2403754e2984016159c5f355659b5a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20M=C3=A5nsson?= Date: Mon, 13 Aug 2018 19:04:53 +0200 Subject: [PATCH] Add Sonos module (#655) --- docs/conf.py | 3 +- i3pystatus/sonos.py | 166 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 i3pystatus/sonos.py diff --git a/docs/conf.py b/docs/conf.py index e477125..cfd0988 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,8 @@ MOCK_MODULES = [ "requests.packages.urllib3.response", "lxml.etree", "requests.adapters", - "exchangelib" + "exchangelib", + "soco" ] for mod_name in MOCK_MODULES: diff --git a/i3pystatus/sonos.py b/i3pystatus/sonos.py new file mode 100644 index 0000000..cf4522a --- /dev/null +++ b/i3pystatus/sonos.py @@ -0,0 +1,166 @@ +from i3pystatus import IntervalModule +import soco + + +class Sonos(IntervalModule): + """ + Controls and displays information from Sonos devices. + + A devices is found by IP, by name, or automatically if no IP or name is \ + supplied. + + .. rubric:: Available formatters + + * `{player_name}` — player name + * `{volume}` — volume from 0 to 100 + * `{muted}` — "M" if muted, else "" + * `{title}` — the title of the current song + * `{artist}` — the artist of the current song + * `{album}` — the album of the current song + * `{duration}` — duration of the current song (%M:%S) + * `{position}` — position in the current song (%M:%S) + * `{state}` — Playing, Paused, Stopped + + Requires: soco (can be installed using pip) + """ + + ip = None + name = None + format = "{state}: {artist} - {title} [{muted}{volume:.0f}%]" + color = "#FFFFFF" + format_no_music = "No music" + color_no_music = "#888888" + format_no_connection = "No connection" + color_no_connection = "#888888" + hide_no_connection = False + + interval = 1 + + settings = ( + ("ip", "Speaker IP address."), + ("name", "Speaker name (used if no IP is given)."), + ("format", "Format used when playing or paused."), + ("color", "Color used when playing or paused."), + ("format_no_music", "Format used when stopped."), + ("color_no_music", "Color used when stopped."), + ("format_no_connection", "Format used if no player is connected."), + ("color_no_connection", "Color used if no player is connected."), + ("hide_no_connection", "Hide output if no player is connected."), + ) + + state_text_map = { + "PLAYING": "Playing", + "TRANSITIONING": "Playing", + "PAUSED_PLAYBACK": "Paused", + "STOPPED": "Stopped", + } + + on_leftclick = "play_pause" + on_upscroll = "incr_vol" + on_middleclick = "toggle_mute" + on_downscroll = "decr_vol" + on_doubleleftclick = "next_song" + + player = None + + def run(self): + if not self.player: + if self.ip: + self.player = soco.SoCo(self.ip) + elif self.name: + self.player = soco.discovery.by_name(self.name) + else: + self.player = soco.discovery.any_soco() + + if not self.player: + self.output = self.output_no_connection + return + + try: + track_info = self.group_coordinator.get_current_track_info() + transp_info = self.group_coordinator.get_current_transport_info() + player_name = self.player.player_name + muted = self.player.mute + volume = self.player.volume + except: + self.output = self.output_no_connection + return + + state = transp_info["current_transport_state"] + + cdict = { + "player_name": player_name, + "volume": volume, + "muted": "M" if muted else "", + "title": track_info["title"], + "artist": track_info["artist"], + "album": track_info["album"], + "duration": track_info["duration"][2:], + "position": track_info["position"][2:], + "state": self.state_text_map[state], + } + + self.data = cdict + + if self.format_no_music is not None and state == "STOPPED": + self.output = { + "full_text": self.format_no_music.format(**cdict), + "color": self.color_no_music + } + else: + self.output = { + "full_text": self.format.format(**cdict), + "color": self.color + } + + @property + def output_no_connection(self): + if self.hide_no_connection: + return None + else: + return { + "full_text": self.format_no_connection, + "color": self.color_no_connection + } + + @property + def group_coordinator(self): + try: + return self.player.group.coordinator + except: + return None + + def play_pause(self): + try: + transp_info = self.group_coordinator.get_current_transport_info() + state = transp_info["current_transport_state"] + if state in ["PLAYING", "TRANSITIONING"]: + self.group_coordinator.pause() + else: + self.group_coordinator.play() + except: + return + + def incr_vol(self): + try: + self.player.volume += 1 + except: + return + + def decr_vol(self): + try: + self.player.volume -= 1 + except: + return + + def toggle_mute(self): + try: + self.player.mute = not self.player.mute + except: + return + + def next_song(self): + try: + self.group_coordinator.next() + except: + return