1 """Component for controlling Pandora stations through the pianobar client."""
3 from __future__
import annotations
5 from datetime
import timedelta
14 from homeassistant
import util
17 MediaPlayerEntityFeature,
22 EVENT_HOMEASSISTANT_STOP,
23 SERVICE_MEDIA_NEXT_TRACK,
25 SERVICE_MEDIA_PLAY_PAUSE,
33 _LOGGER = logging.getLogger(__name__)
37 SERVICE_MEDIA_NEXT_TRACK:
"n",
38 SERVICE_MEDIA_PLAY_PAUSE:
"p",
39 SERVICE_MEDIA_PLAY:
"p",
40 SERVICE_VOLUME_UP:
")",
41 SERVICE_VOLUME_DOWN:
"(",
44 CURRENT_SONG_PATTERN = re.compile(
r'"(.*?)"\s+by\s+"(.*?)"\son\s+"(.*?)"', re.MULTILINE)
45 STATION_PATTERN = re.compile(
r'Station\s"(.+?)"', re.MULTILINE)
51 add_entities: AddEntitiesCallback,
52 discovery_info: DiscoveryInfoType |
None =
None,
54 """Set up the Pandora media player platform."""
61 def _stop_pianobar(_event):
64 hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_pianobar)
69 """A media player that uses the Pianobar interface to Pandora."""
71 _attr_media_content_type = MediaType.MUSIC
74 _attr_supported_features = (
75 MediaPlayerEntityFeature.PAUSE
76 | MediaPlayerEntityFeature.TURN_ON
77 | MediaPlayerEntityFeature.TURN_OFF
78 | MediaPlayerEntityFeature.NEXT_TRACK
79 | MediaPlayerEntityFeature.SELECT_SOURCE
80 | MediaPlayerEntityFeature.PLAY
84 """Initialize the Pandora device."""
97 """Turn the media player on."""
100 self.
_pianobar_pianobar = pexpect.spawn(
"pianobar")
101 _LOGGER.debug(
"Started pianobar subprocess")
103 [
"Receiving new playlist",
"Select station:",
"Email:"]
110 "The pianobar client is not configured to log in. "
111 "Please create a configuration file for it as described at "
112 "https://www.home-assistant.io/integrations/pandora/"
123 self.
_attr_state_attr_state = MediaPlayerState.IDLE
127 """Turn the media player off."""
129 _LOGGER.warning(
"Pianobar subprocess already stopped")
133 _LOGGER.debug(
"Stopped Pianobar subprocess")
135 except pexpect.exceptions.TIMEOUT:
137 os.killpg(os.getpgid(self.
_pianobar_pianobar.pid), signal.SIGTERM)
138 _LOGGER.debug(
"Killed Pianobar subprocess")
144 """Send play command."""
146 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
150 """Send pause command."""
152 self.
_attr_state_attr_state = MediaPlayerState.PAUSED
156 """Go to next track."""
162 """Title of current playing media."""
167 """Choose a different Pandora station and play it."""
171 station_index = self.
source_listsource_list.index(source)
173 _LOGGER.warning(
"Station %s is not in list", source)
175 _LOGGER.debug(
"Setting station %s, %d", source, station_index)
177 self.
_pianobar_pianobar.sendline(f
"{station_index}")
179 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
182 """Send a station list command."""
185 self.
_pianobar_pianobar.expect(
"Select station:", timeout=1)
186 except pexpect.exceptions.TIMEOUT:
190 self.
_pianobar_pianobar.expect(
"Select station:")
193 """Query pianobar for info about current media_title, station."""
202 """Query system for info about current track."""
206 match_idx = self.
_pianobar_pianobar.expect(
208 rb
"(\d\d):(\d\d)/(\d\d):(\d\d)",
211 "Receiving new playlist",
214 except pexpect.exceptions.EOF:
215 _LOGGER.warning(
"Pianobar process already exited")
224 _LOGGER.warning(
"On unexpected station list page")
229 _LOGGER.debug(
"Received new playlist list")
232 response = self.
_pianobar_pianobar.before.decode(
"utf-8")
236 """Update current station."""
237 if station_match := re.search(STATION_PATTERN, response):
239 _LOGGER.debug(
"Got station as: %s", self.
_attr_source_attr_source)
241 _LOGGER.warning(
"No station match")
244 """Update info about current song."""
245 if song_match := re.search(CURRENT_SONG_PATTERN, response):
250 ) = song_match.groups()
253 _LOGGER.warning(
"No song match")
255 @util.Throttle(MIN_TIME_BETWEEN_UPDATES)
257 """Get the song position and duration.
259 It's hard to predict whether or not the music will start during init
260 so we have to detect state by checking the ticker.
268 ) = self.
_pianobar_pianobar.match.groups()
269 time_remaining =
int(cur_minutes) * 60 +
int(cur_seconds)
273 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
275 self.
_attr_state_attr_state = MediaPlayerState.PAUSED
279 """Log grabbed values from console."""
281 "Before: %s\nMatch: %s\nAfter: %s",
288 """Send a command to Pianobar."""
289 command = CMD_MAP.get(service_cmd)
290 _LOGGER.debug(
"Sending pinaobar command %s for %s", command, service_cmd)
292 _LOGGER.warning(
"Command %s not supported yet", service_cmd)
294 self.
_pianobar_pianobar.sendline(command)
297 """List defined Pandora stations."""
299 station_lines = self.
_pianobar_pianobar.before.decode(
"utf-8")
300 _LOGGER.debug(
"Getting stations: %s", station_lines)
302 for line
in station_lines.split(
"\r\n"):
303 if match := re.search(
r"\d+\).....(.+)", line):
304 station = match.group(1).strip()
305 _LOGGER.debug(
"Found station %s", station)
308 _LOGGER.debug(
"No station match on %s", line)
313 """Clear buffer from pexpect.
315 This is necessary because there are a bunch of 00:00 in the buffer
319 while not self.
_pianobar_pianobar.expect(
".+", timeout=0.1):
321 except pexpect.exceptions.TIMEOUT:
323 except pexpect.exceptions.EOF:
328 """Verify that Pianobar is properly installed."""
329 pianobar_exe = shutil.which(
"pianobar")
334 "The Pandora integration depends on the Pianobar client, which "
335 "cannot be found. Please install using instructions at "
336 "https://www.home-assistant.io/integrations/media_player.pandora/"
None schedule_update_ha_state(self, bool force_refresh=False)
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)