Play wav file in Sonos

Can someone confirm if it is possible to play an arbitary wav or mp3 file on Sonos? I had a look at the play_media service and it doesn’t seem happy with my usage of the media_content_id - will I need to figure a way to host the files so Sonos can stream them and provide it with a URL, or can HA do the hard work and just push a local file to Sonos?

Assuming from the lack of answers that the answer is no, I ended up building an AppDaemon app to accept a request for media and then stream it to the Sonos. I added it to an existing App that handles TTS, and single threads all play requests so they happen sequentially without overwriting each other.

The full app is here:

import appdaemon.appapi as appapi
from queue import Queue
from threading import Thread
import time
from urllib.parse import quote
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
import soco
#
# App to manage announcements via TTS and stream sound files to Sonos (requires soco)
#
# Provides methods to enqueue TTS and Media file requests and make sure that only one is executed at a time
# Volume of the media player is set to a specified level and then restored afterwards
#
# Args:
#
# player - media player to use for announcements
# zone = Sonos Name for the Zone
# base = base directory for media with trailing "\"
# ip = IP address of machine running this app
# port = to stream from
#
# To use from another APP:
# TTS:
# sound = self.get_app("Sound")
# sound.tts(text, volume, duration)
#
# SOUND:
# sound = self.get_app("Sound")
# sound.play(file, volume, duration)
# file is the path of the file to play relative to "base"
# Release Notes
#
# Version 1.0:
#   Initial Version


class HttpServer(Thread):
  """A simple HTTP Server in its own thread"""

  def __init__(self, port):
      super(HttpServer, self).__init__()
      self.daemon = True
      handler = SimpleHTTPRequestHandler
      self.httpd = TCPServer(("", port), handler)

  def run(self):
      """Start the server"""
      print('Start HTTP server')
      self.httpd.serve_forever()

  def stop(self):
      """Stop the server"""
      print('Stop HTTP server')
      self.httpd.socket.close()

class Sound(appapi.AppDaemon):

  def initialize(self):
    
    # Create Queue
    self.queue = Queue(maxsize=0)

    # Create worker thread
    t = Thread(target=self.worker)
    t.daemon = True
    t.start()
    
    # Settings
    machine_ip = self.args["ip"]
    port = int(self.args["port"])
    zone_name = self.args["player"]
    # Setup and start the http server
    server = HttpServer(int(self.args["port"]))
    server.start()

  def play_media(self, path, volume, length):
    path = self.args["base"] + path
    #path = os.path.join(
    #    *[quote(part) for part in os.path.split(path)]
    #)
    netpath = 'http://{}:{}/{}'.format(self.args["ip"], self.args["port"], path)

    for zone in soco.discover():
        if zone.player_name == self.args["Zone"]:
            break

    number_in_queue = zone.add_uri_to_queue(netpath)
    zone.play_from_queue(number_in_queue - 1)
    
  def worker(self):
    while True:
      try:
        # Get text to say
        data = self.queue.get()
        # Save current volume
        volume = self.get_state(self.args["player"], attribute="volume_level")
        # Set to the desired volume
        self.call_service("media_player/volume_set", entity_id = self.args["player"], volume_level = data["volume"])
        if data["type"] == "tts":
          # Call TTS service
          self.call_service("tts/google_say", entity_id = self.args["player"], message = data["text"])
        if data["type"] == "play":
          # Call Media service
          self.play_media(data["path"], data["volume"], data["length"])

        # Sleep to allow message to complete before restoring volume
        time.sleep(int(data["length"]))
        # Restore volume
        self.call_service("media_player/volume_set", entity_id = self.args["player"], volume_level = volume)
        # Set state locally as well to avoid race condition
        self.set_state(self.args["player"], attributes = {"volume_level": volume})
      except:
        self.log("Error")
        self.log(sys.exc_info())

      # Rinse and repeat
      self.queue.task_done()

       
  def tts(self, text, volume, length):
    self.queue.put({"type": "tts", "text": text, "volume": volume, "length": length})
    
  def play(self, path, volume, length):
    self.queue.put({"type": "play", "path": path, "volume": volume, "length": length})

Have you tried this?

service: media_player.play_media

  #  data:
  #    entity_id: media_player.lounge
  #    media_content_id: http://192.168.1.41:8123/local/doorbell.mp3
  #    media_content_type: audio/mp3

I tried the AppDaemon equivalent, using hass to serve up the files but got errors - I think I was missing the content type though - I’ll give it a try and if it works I can make my app a lot simpler, thanks!

Sadly I still get an error:

Jan 14 09:51:06 Pegasus hass[25810]: INFO:homeassistant.core:Bus:Handling <Event call_service[L]: service_call_id=140653490168664-12, service=play_media, service_data=entity_id=media_player.living_room, media_content_type=audio/mp3, media_content_id=http://192.168.1.20:8123/local/media/GFChime/3_4_Collab_BT.mp3, domain=media_player>
Jan 14 09:51:06 Pegasus hass[25810]: ERROR:aiohttp.server:Error handling request
Jan 14 09:51:06 Pegasus hass[25810]: Traceback (most recent call last):
Jan 14 09:51:06 Pegasus hass[25810]:   File "/home/hass/deps/aiohttp/web_server.py", line 61, in handle_request
Jan 14 09:51:06 Pegasus hass[25810]:     resp = yield from self._handler(request)
Jan 14 09:51:06 Pegasus hass[25810]:   File "/home/hass/deps/aiohttp/web.py", line 249, in _handle
Jan 14 09:51:06 Pegasus hass[25810]:     resp = yield from handler(request)
Jan 14 09:51:06 Pegasus hass[25810]:   File "/usr/lib/python3.5/asyncio/coroutines.py", line 209, in coro
Jan 14 09:51:06 Pegasus hass[25810]:     res = yield from res
Jan 14 09:51:06 Pegasus hass[25810]:   File "/usr/lib/python3.5/asyncio/coroutines.py", line 209, in coro
Jan 14 09:51:06 Pegasus hass[25810]:     res = yield from res
Jan 14 09:51:06 Pegasus hass[25810]:   File "/export/hass/hass_env/lib/python3.5/site-packages/homeassistant/components/http/static.py", line 91, in static_middleware_handler
Jan 14 09:51:06 Pegasus hass[25810]:     resp = yield from handler(request)
Jan 14 09:51:06 Pegasus hass[25810]:   File "/home/hass/deps/aiohttp/web_urldispatcher.py", line 486, in _handle
Jan 14 09:51:06 Pegasus hass[25810]:     ret = yield from self._file_sender.send(request, filepath)
Jan 14 09:51:06 Pegasus hass[25810]:   File "/export/hass/hass_env/lib/python3.5/site-packages/homeassistant/components/http/static.py", line 25, in send
Jan 14 09:51:06 Pegasus hass[25810]:     if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
Jan 14 09:51:06 Pegasus hass[25810]:   File "multidict/_multidict.pyx", line 124, in multidict._multidict._Base.__getitem__ (multidict/_multidict.c:3486)
Jan 14 09:51:06 Pegasus hass[25810]:   File "multidict/_multidict.pyx", line 119, in multidict._multidict._Base._getone (multidict/_multidict.c:3421)
Jan 14 09:51:06 Pegasus hass[25810]: KeyError: "Key not found: 'Accept-Encoding'"

Anyone have any ideas how to fix this?

FWIW, this also doesn’t seem to play nicely with Google Cast devices. I can hear the connection happen, but no audio will get played. Here’s the section of my AppDaemon script.

import appdaemon.appapi as appapi
import heapq
import threading
import time
import sys
import os
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from threading import Thread
from tempfile import NamedTemporaryFile
from mutagen.mp3 import MP3
from gtts import gTTS

class tts_player(appapi.AppDaemon):
    
    def initialize(self):
        ...
        server = HttpServer(9985)
        server.start()

    def play_media(self):
        test = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."

        try:
            #with NamedTemporaryFile() as f:
            f = '/tmp/tts/test.mp3'
            filename, duration = self.create_tts(message=test, file=f)
            self.log('{} is {}s long.'.format(filename, duration))
            time.sleep(duration)
            self.call_service('media_player/play_media',
                              entity_id='media_player.bedroom',
                              media_content_id='http://192.168.1.113:9985{}'.format(filename),
                              media_content_type='MUSIC')
            time.sleep(duration * 3)
        except Exception as e:
            self.error(e)
            self.error(sys.exec_info())
            server.stop()

I finally managed to get this working using HASS to stream the media so I could remove the HTTP server from the App - much simpler!

Here is the latest version:

import appdaemon.appapi as appapi
from queue import Queue
from threading import Thread
import time
#
# App to manage announcements via TTS and stream sound files to Sonos
#
# Provides methods to enqueue TTS and Media file requests and make sure that only one is executed at a time
# Volume of the media player is set to a specified level and then restored afterwards
#
# Args:
#
# player - media player to use for announcements
# base = base directory for media - this will be a subdirectory under <home assistant config dir>/www
# ip = IP address of machine running this app
# port = HASS port
#
# To use from another APP:
# TTS:
# sound = self.get_app("Sound")
# sound.tts(text, volume, duration)
# duration should be set to longer than the expected duration of the speech
#
# e.g.:
#
# sound = self.get_app("Sound")
# sound.tts("Warning: Intuder alert", 0.5, 10)#
#
# SOUND:
# sound = self.get_app("Sound")
# sound.play(file, volume, content_type, duration)
# file is the path of the file to play relative to "base"
# Content type is the mime type of the media e.g. "audio/mp3" or "audio/wav"
# duration should be set to longer than the expected duration of the media file
#
# e.g.:
# sound = self.get_app("Sound")
# sound.play("warning.wav", "audio/wav", 0.5, 10)
#
# Release Notes
#
# Version 1.0:
#   Initial Version

class Sound(appapi.AppDaemon):

  def initialize(self):
    
    # Create Queue
    self.queue = Queue(maxsize=0)

    # Create worker thread
    t = Thread(target=self.worker)
    t.daemon = True
    t.start()
    
  def worker(self):
    while True:
      try:
        # Get text to say
        data = self.queue.get()
        # Save current volume
        volume = self.get_state(self.args["player"], attribute="volume_level")
        # Set to the desired volume
        self.call_service("media_player/volume_set", entity_id = self.args["player"], volume_level = data["volume"])
        if data["type"] == "tts":
          # Call TTS service
          self.call_service("tts/google_say", entity_id = self.args["player"], message = data["text"])
        if data["type"] == "play":
          netpath = netpath = 'http://{}:{}/local/{}/{}'.format(self.args["ip"], self.args["port"], self.args["base"], data["path"])
          self.call_service("media_player/play_media", entity_id = self.args["player"], media_content_id = netpath, media_content_type = data["content"])

        # Sleep to allow message to complete before restoring volume
        time.sleep(int(data["length"]))
        # Restore volume
        self.call_service("media_player/volume_set", entity_id = self.args["player"], volume_level = volume)
        # Set state locally as well to avoid race condition
        self.set_state(self.args["player"], attributes = {"volume_level": volume})
      except:
        self.log("Error")
        self.log(sys.exc_info())

      # Rinse and repeat
      self.queue.task_done()

       
  def tts(self, text, volume, length):
    self.queue.put({"type": "tts", "text": text, "volume": volume, "length": length})
    
  def play(self, path, content, volume, length):
    self.queue.put({"type": "play", "path": path, "content": content, "volume": volume, "length": length})

1 Like

That’s great! It looks like this still isn’t working well with Google Cast devices.

Here’s my latest as well. Shows implementation of heapq so that each media_player can have their own priority queue. As you can tell by some function names and the module name itself, I started with your notifications queue as a shell :wink:

Additionally, if you have your users install a single external library, then understanding the proper time.sleep duration is trivial. It’s simple n+0.5 or n+1 depending on comfort. Additionally, it’s cross-platform and requires no external dependencies.

import appdaemon.appapi as appapi
import heapq
import threading
import time
import os
from threading import Thread
from tempfile import NamedTemporaryFile
from mutagen.mp3 import MP3
from gtts import gTTS

class Announce(appapi.AppDaemon):
    
    def initialize(self):
        self.priority_levels = {
            "low": 2,
            "medium": 1,
            "high": 0
        }

        self._queue = []
        self._index = 0

        t = threading.Thread(target=self.worker)
        t.daemon = True
        t.start()

    def pop(self):
        return heapq.heappop(self._queue)[-1]

    def worker(self):
        while True:
            try:
                media_player, message, volume_level = self.pop()
                before_volume = self.get_state(media_player, attribute='volume_level')

                #with NamedTemporaryFile() as f:
                filename, duration = self.create_tts(message=message)
                self.call_service('media_player/volume_set', entity_id=media_player, volume_level=volume_level)
                self.call_service('media_player/play_media',
                                  entity_id='media_player.bedroom',
                                  media_content_id='http://192.168.1.113:8123{}'.format(filename),
                                  media_content_type='audio/mp3')
                time.sleep(duration + 1)

                self.call_service('media_player/volume_set', entity_id=media_player, volume_level=before_volume)
                self.set_state(media_player, attributes={'volume_level': before_volume})
            except IndexError:
                pass
            except Exception as e:
                self.error("{}\n{}".format(e, sys.exec_info()))

    def create_tts(self, message, file='/tmp/tts', lang='en-us'):
        tts = gTTS(text=message, lang=lang)
        
        try:
            tts.write_to_fp(file)
            filename = file.name
        except AttributeError:
            if file == '/tmp/tts':
                if not os.path.isdir('/tmp/tts'):
                    os.mkdir('/tmp/tts')

                temp_fn = time.time()
                file = '{}/{}.mp3'.format(file, str(temp_fn-int(temp_fn))[2:])
            
            tts.save(file)
            filename = file

        return filename, int(MP3(file).info.length)
    
    def say(self, message, media_player, volume=0.65, priority='low'):       
        self._index += 1
        heapq.heappush(self._queue, (self.priority_levels[priority], self._index, (media_player, message, volume)))
1 Like

I was following this thread and also wanted to play a wav file through my sonos but didn’t want to install appdaemon. The docs were pretty good and so here’s a working automation that plays a sound when motion is detected. First, I couldn’t make wav files work, but mp3 files work fine. Also, you’ll need to put your mp3 files in a directory titled www that reside in your .homeassistant directory.

- alias: 'Dog bark'
  initial_state: false
  hide_entity: false
  trigger:
    - platform: state
      entity_id: binary_sensor.motionsensor_sensor_23_0
      state: 'on'
    - platform: state
      entity_id: binary_sensor.motionsensor_sensor_8_0
      state: 'on'
  condition:
    condition: state
    entity_id: input_boolean.alarm
    state: 'on'
  action:
    - service: media_player.volume_set
      data:
        entity_id: media_player.family_room
        volume_level: 0.75
    - service: media_player.play_media
      data:
        entity_id: media_player.family_room
        media_content_id: http://192.168.1.45:8123/local/dog.mp3
        media_content_type: music
1 Like

Here’s what I’ve been doing to send an mp3 to my sonos. I’m running a small http server on another port, on the same machine that runs hass, just to serve a few mp3s. This one is austin powers “you’ve got mail, baby, yeah!” :slight_smile:

alias: Play mail announcement
sequence:
  - service: media_player.sonos_snapshot
    data:
      entity_id: media_player.office

  - service: media_player.play_media
    data:
      entity_id: media_player.office
      media_content_type: MUSIC
      media_content_id: "http://my-hass-host:9000/apmail.mp3"

  - delay:
      seconds: 4

  - service: media_player.sonos_restore
    data:
      entity_id: media_player.office

only certain bitrates/formats/sample rates are supported by sonos, so I had to try a few until I had an mp3 file that worked.

Looks like it’d be easier to use the www dir under the .homeassistant dir in future. Thanks for posting that.

1 Like

I checked the property of my mp3 file and it is showing 160kbps and not sure how to understand it as bit rate for Sonos that has limit of 44.1KHz 16 bit resolution.