[PLUGIN] Pandora Plugin 2.0

idle idea. Is a song still playable after it has been played? Could a “played queue” be maintained and songs be replayed?

When the tracks are downloaded from the server, each song has a unique URL link associated with it. The link lasts for about 45 minutes or so and then expires. The expiration time seems to vary a bit.

If the track is played all the way through, or is played for a certain amount of time, it cannot be revisited. I have been able to start and stop a track a few times, but this fails after a few attempts.

I have been working on a new version with MQTT support that I mentioned in a previous post. It wasn’t much to add this feature, and it’s optional. The station list and the current station are published and they can be grabbed. I also added a function to change the station that one could access with the Websocket API. That’s a little tricky to get working if you code it yourself, but many home automation software programs offer a generic interface.

I have a mostly-working interface with my Google Home devices at the moment with Home Assistant. Right now I can play and change Pandora stations in Volumio if the plugin is enabled. I installed the mpd2chromecast plugin and it works really well.

It seems to me that I found a few small bugs with my new version. I tried to clean up and consolidate some of the code.

So I pushed a new 2.8.0 version to the develop branch on GitHub. It’s working for me but I haven’t sent it up the Volumio pipeline yet.

I’m pretty sure I left the instructions for checking out a branch in the Readme.md file. You won’t be able to simply install this develop version from the main menu (yet). You might want to uninstall the old version first. I shifted the code around a lot since the last version in an attempt to clean things up.

I haven’t worked on it in a couple of weeks and I’m a little hazy on all the changes at this point, but I listed just about all of them in the Readme.md file which you can read on my fork.

I run Home Assistant to automate a few things here in my house. It’s not like the whole house is automated – really I just have the room here with the machines and a couple of switches in various rooms.

What I did with the code is to send the station list to my MQTT server and then Home Assistant put it into an input_select control. Then, I can change the station in HA, and that gets sent back through the Volumio API and it’s done. The cool thing is that it can be changed with a voice command through Google Assistant (phone, Google Mini, etc). And I guess Alexa would do it as well. That part is really handled by Home Assistant once you set that up.

So, it’s a joint effort between Home Assistant and the Volumio Pandora Plugin to change the station.

All that the plugin does is to send the station list to MQTT (if you want). So you could do something else with the data, whatever you needed. And if someone wants more data sent to MQTT, I could do that as well (song title / artist / album / album art URL / etc.)

Is it worth it? Well… I think it is because it’s a challenge! But maybe you don’t. You don’t have to use this feature at all. I mean, you can just use your phone or PC or Mac or whatever to control Volumio.

If anyone has any interest in my Home Assistant configuration, I can post it. I have the feeling this is going to go over like a lead balloon. Just let me know and I’ll fill you in.

I am running version 2.8 with the MQTT broker. Can you share your HA config that allows the Pandora Station list to be used to create a input select list?

Yes, absolutely. I have not tested this in a while – I hope that this works or will be a good starting point for you.

You will need to install the python-script and pyscript custom component:

I should probably choose one or the other. We’ll get that sorted.

NOTE: It may not totally work but I will help you fix it. You’re the first to show interest. You can increase the verbosity of the Home Assistant logger for individual components in configuration.yaml:

# Logging
logger:
  default: warning
  # logs:
    # log level for specific components
    # homeassistant.components.python_script: debug
    # homeassistant.components.media_player.kodi: debug
    # homeassistant.components.shell_command: debug

It is broken into several files. I named the thing ‘hornet’. There was some kind of name conflict. It’s pretty dumb but that’s what I have at the moment.

Add to scripts.yaml:

## VOLUMIO SCRIPTS ###

play_hornet_station:
  alias: "Play Hornet Station"
  sequence:
  - service: shell_command.play_volumio_pandora_station

google_sync_devices: # called by update_options.py
  alias: "Sync Home Assistant to Google Assistant"
  sequence:
  - service: google_assistant.request_sync
    data_template:
      agent_user_id: !secret agent_user_id # need to set this to a key in your google action setup

Add
shell_command: !include shell_command.yaml to configuration.yaml
OR
Add a shell_command: section to configuration.yaml:

# Call with </config/ws_send_volumio_command.py> <service> <service_method/procedure> <dataJSON>
# play_volumio_pandora_station: "\"/config/ws_volumio_callMethod.py pandora clearAndPlayStation \"{\"stationName\": \"{{ states(\"input_select.pandora_station\") }}\"}\""
play_volumio_pandora_station: >-
  {%- set newStationName = states('input_select.hornet_station') %}
  {% set dataJSON = "{'stationName': '" + newStationName + "'}" %}
  {% set serviceName = "pandora" %}
  {% set pluginMethod = "clearAndPlayStation" %}
  /config/ws_send_volumio_command.py {{ [serviceName, pluginMethod] | join(' ') }} "{{ dataJSON }}"

Add to input_select.yaml:

hornet_station:
  name: Hornet
  options:
    - None

/config/ws_send_volumio_command.py:

#!/usr/bin/env python
"""
WS interface to Volumio'

Call with </config/ws_send_volumio_command.py> <service> <service_method/procedure> <dataJSON>
'music_service/' is prepended to <service> parameter
i.e. 'music_service/pandora' if <service> == 'pandora'

See https://websockets.readthedocs.io/en/stable/intro.html for WS examples
See https://volumio.github.io/docs/API/WebSocket_APIs.html for Volumio WS API
"""

import sys
import json
import asyncio
import websockets

URI = 'ws://volumio.lan:3000'
PANDORA_SERVICE = 'music_service/pandora'
CMD = 'callMethod'
NUM_ARGS = 4 # really 3 arguments

async def send_volumio_cmd(cmd, payload):
    'Send websocket command to Volumio'

    async with websockets.connect(URI) as websocket:
        await websocket.send(cmd, payload)

        response = await websocket.recv()
        print('response: %s' % response)

async def process_args():
    'Process command-line arguments'

    volumio_payload = {
        'endpoint': '',
        'method': '',
        'data': {}
    }

    if len(sys.argv) > NUM_ARGS:
        print('You have specified too many arguments:\n%s' % sys.argv)
        sys.exit()
    if len(sys.argv) < NUM_ARGS:
        print('You have specified too few arguments:\n%s' % sys.argv)
        sys.exit()

    try:
        volumio_payload['endpoint'] = 'music_service/' + sys.argv[1]
        volumio_payload['method'] = sys.argv[2]
        volumio_payload['data'] = json.loads(sys.argv[3])

    except Exception as err:
        print('Argument is not a JSON string: %s' % err)
        sys.exit()

    await send_volumio_cmd(CMD, volumio_payload)

# call process_args() which calls main async function and quits
asyncio.get_event_loop().run_until_complete(process_args())

drop this in your /config/pyscript folder. name it update_options.py:

@service
def update_options(entity_id, options_csv, retain_state='hell no'):
    retain_state = 'hell yes' in retain_state.lower()
    if entity_id is not None and options_csv is not None:
        options = [ o.strip() for o in options_csv.split(",") ]

        if entity_id.startswith('input_select') and entity_id.count('.') == 1:
            input_select.set_options(entity_id=entity_id, options=options)

            if retain_state:
                current_state = entity_id

                if current_state is not None and current_state in options:
                    input_select.select_option(entity_id=entity_id, option=current_state)
                    
    script.google_sync_devices()

I found the input_select update script on the Home Assistant forums somewhere. It is possible that I might have changed a string in the index.js code to something (i.e. not ‘hell yes’ or ‘hell no’) I haven’t been working on it for a couple of months but I’m happy to take another look.

I think I posted all the files, but I may have forgotten one.

Good luck and let me know what works, and more likely, what doesn’t, and we’ll go from there!

Are the station names that are published via the plugin to MQTT used at all?

This is the error I am getting in my logs.

Logger: homeassistant.components.shell_command
Source: helpers/template.py:355
Integration: Shell Command (documentation, issues)
First occurred: 1:05:39 AM (2 occurrences)
Last logged: 1:06:22 AM

Error rendering command template: UndefinedError: ‘newStationName’ is undefined
Traceback (most recent call last):
File “/usr/src/homeassistant/homeassistant/helpers/template.py”, line 353, in async_render
render_result = compiled.render(kwargs)
File “/usr/local/lib/python3.8/site-packages/jinja2/environment.py”, line 1090, in render
self.environment.handle_exception()
File “/usr/local/lib/python3.8/site-packages/jinja2/environment.py”, line 832, in handle_exception
reraise(*rewrite_traceback_stack(source=source))
File “/usr/local/lib/python3.8/site-packages/jinja2/_compat.py”, line 28, in reraise
raise value.with_traceback(tb)
File “”, line 1, in top-level template code
jinja2.exceptions.UndefinedError: ‘newStationName’ is undefined

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File “/usr/src/homeassistant/homeassistant/components/shell_command/init.py”, line 48, in async_service_handler
rendered_args = args_compiled.async_render(
File “/usr/src/homeassistant/homeassistant/helpers/template.py”, line 355, in async_render
raise TemplateError(err) from err
homeassistant.exceptions.TemplateError: UndefinedError: ‘newStationName’ is undefined

Blockquote

The service call refers to an option_csv. Is this something that you generate from the pandora plugin station list?

Thanks

Looks Iike I didn’t include this file. Drop it in /config/python_scripts as update_options.py:

# Set input_select options from a comma-delimited string
# See https://community.home-assistant.io/t/input-select-set-options-also-update-the-current-state-too-what-to-do/153340/11
# Modified 11/30/2020
# Cyber Monday Bitch!

entity_id = data.get('entity_id')
options = data.get('options')
# retain_state in data_template
# either 'hell yes' or 'hell no':
# True/False/Yes/No/Y/N evaluate to boolean
# we need type str! surround with quotes!
retain_state = data.get('retain_state', 'hell no')
retain_state = 'hell yes' in retain_state.lower()

google_sync_script_name = 'script.google_sync_devices'
google_sync_data = {'entity_id': google_sync_script_name}

if entity_id is not None and options is not None:
    options = [ o.strip() for o in options.split(",") ]

    if entity_id.startswith('input_select') and entity_id.count('.') == 1:
        set_options_data = {'entity_id': entity_id, 'options': options}
        hass.services.call('input_select', 'set_options', set_options_data)

        if retain_state:
            current_state = hass.states.get(entity_id)

            if current_state is not None and current_state.state in options:
                select_option_data = {'entity_id': entity_id, 'option': current_state.state}
                hass.services.call('input_select', 'select_option', select_option_data)

        hass.services.call('script', 'turn_on', google_sync_data) # sync Google devices to HA

I’ll keep digging here.

I’m fairly certain I have not implemented that yet. I thought it might help others to expose that information. But honestly, it appears I haven’t looked at this since around November and the details are hazy at the moment. :smiley:

@Drizzay Just curious, did this work for you, or do we need to take another look at this? I had this mostly working but there were a few kinks if I recall.

No it hasn’t, but I want to check my understanding of what should be happening. Is the hornet input selection options supposed to be updated with the list of Pandora stations that are available from the plugin on volumio?

1 Like

Yes. So if you’re not seeing an update there, we have to start at that point.

If I recall, setting the input_select option should fire off a shell_command, which calls a python script that sends the web socket (ws) command to the Pandora plugin, which in turn changes the station. Yeah.

It’s a house of cards, but believe it or not, it’s worked a few times.

Sometimes the station names throws if off and Google Assistant plays the band through Spotify or Pandora directly (whatever you’ve set as a music service).

I didn’t clean the code up and then I got involved doing something else.

The Google Assistant voice command is a little weird. See this post on the HA forum:

I think it can be done.

Crap. So it looks like I forgot one more sensor YAML file. Sorry about that. Your comment clued me in:

## VOLUMIO SENSORS ###

- platform: mqtt
  name: Volumio Pandora stationData Sensor
  state_topic: 'volumio/pandora/stationData'
  value_template: '{{ value_json }}'
  json_attributes_topic: 'volumio/pandora/stationData'

I’ll have to make a new repository on GitHub with all of these files so they’ll be easier to handle. I’m about to grab some dinner but I’ll do it tonight.

Here is the repository. I think I have it all in there now:

That should make this “hack” easier to work on.

From what I can see, that last file I posted was superseded by the input_select_set_options_hornet_station automation found in the automations_volumio.yaml file, so you probably don’t need it. I left it in the repository but I commented it out and left a small note.

I’ll have to take a look at the scripts in python_scripts as well.

Anyway, it’s a start. We’ll get it sorted.

Will take a look at it now. Thanks for all the effort.

One of the scripts is looking for an MQTT topic that doesn’t exist in my version of the plugin. It is looking for ‘volumio/pandora/stationData’. The only volumio topic that exists is. ‘volumio/pandora/stationName’

Also for some reason when the shell command runs it isn’t setting the New Station variable at run time. When I test it in the developer template tool it works ok.

Thanks for taking a look at that.

Okay, try these commands from your Volumio machine. You’ll have to connect to it by SSH. I’m pretty sure you know how to do that – this is for anyone else watching.

cd /data/plugins/music_service/pandora
grep "StationDataNoRadio," pandora_handler.js

That should bring back this line:

That part of the code is publishing data to the volumio/pandora/stationData topic.

If you don’t get a result, there may just be something a little screwy with your installation. I’d just grab it again.

mkdir pandora-tmp
cd pandora-tmp
git clone https://github.com/truckershitch/volumio-plugins.git
cd volumio-plugins
git checkout develop
rm -rf /data/plugins/music_service/pandora
cp -R plugins/music_service/pandora /data/plugins/music_service/pandora

Then, you can just restart the Volumio machine or you can do this:

volumio vrestart

And if you want to follow the Volumio logs:

journalctl -f

I don’t think this version has been committed to the Pandora code base yet. I’ll get on that as I think it’s ready. After that it will be a simple upgrade as usual from the main menu.

All the data for this plugin is in /data/plugins/music_service/pandora and the configuration is in /data/configuration/music_service/pandora/config.json. You can safely nuke these any time you want to, or back them up.

The code exists in the pandora_handler.js file but it isn’t publishing the info via MQTT. I recopied and reinstalled from the development branch.

Okay, let me take a look at it on my end here while I have an hour or two. I’ll get back with you in a little while.